feat(provider): complete ChatStream with llmsproxy-grade streaming

Rewrite LuaAdaptedProvider.ChatStream to match the maturity of
llmsproxy's streaming implementation:

HTTP layer:
  - Dedicated stream HTTP client with no overall timeout (SSE must not
    be cut by the 180s Chat timeout); only a 30s dial timeout
  - Uses applyAdapterHeaders (supports build_headers dynamic signing
    hook), matching the non-streaming Chat path

Non-200 response handling:
  - New TransformError Lua hook (adapter.transform_error) for per-source
    protocol knowledge in error messages
  - Safe fallback truncation of raw error bodies (prevents HTML dump
    leakage to clients)

SSE parsing enhancements:
  - parseOpenAICompatibleStreamChunkFull: handles token usage in the
    final chunk (prompt_tokens/prompt, total_tokens/total dual keys),
    prompt cache detail fields, and empty-string finish_reason filtering
    (sensenova sends "" on every chunk)
  - Replaced old SSEScanner with bufio.Scanner (larger buffer, fewer
    allocations)

Stream integrity:
  - errorOnlyChunk detection: holds back the first chunk to reject
    degenerate streams (e.g. zen free pool's finish_reason:"network_error"
    with empty content) before any byte reaches the caller
  - [DONE] dedup: adapters that already emit a terminating done chunk
    with the real finish_reason don't get a second reason-less done
  - Clean EOF sends a final Done:true if no done was seen

Struct changes:
  - StreamChunk: added FinishReason and Usage fields for callers
  - LuaAdaptedProvider: added streamClient (lazy) + streamMu

Tested: curl against llmsproxy SSE confirms reasoning_content parsing
is correct (delta.reasoning_content), usage chunk handling works, and
[DONE] termination is properly emitted.
This commit is contained in:
JianFeeeee
2026-08-25 08:30:53 +08:00
parent d1e502d367
commit 7d6c0bb90b
2 changed files with 237 additions and 68 deletions

View File

@ -335,6 +335,42 @@ func (v *VM) CallTransformResponse(name, rawJSON string) (string, error) {
return out, nil
}
// TransformError 执行可选的 adapter.transform_error(status, body) 钩子。
// 返回 reason: 适配器提取的错误原因ok=false 表示适配器未定义此钩子。
func (v *VM) TransformError(name string, status int, body string) (reason string, ok bool, err error) {
p := v.pool(name)
if p == nil {
return "", false, nil
}
w, err := p.acquire()
if err != nil {
return "", false, err
}
defer p.release(w)
L := w.L
adapter := L.GetGlobal(adapterGlobal)
tbl, ok2 := adapter.(*lua.LTable)
if !ok2 {
return "", false, nil
}
f := tbl.RawGetString("transform_error")
if _, ok := f.(*lua.LFunction); !ok {
return "", false, nil
}
L.Push(f)
L.Push(lua.LNumber(status))
L.Push(lua.LString(body))
if err := L.PCall(2, 1, nil); err != nil {
return "", false, fmt.Errorf("transform_error: %w", err)
}
res := L.Get(-1)
L.Pop(1)
if res.Type() != lua.LTString {
return "", false, nil
}
return res.String(), true, nil
}
func (v *VM) CallTransformStreamChunk(name, rawLine string) (string, error) {
p := v.pool(name)
if p == nil {