agent-accelerator 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/agent.ts +830 -116
- package/src/agent/context.ts +35 -0
- package/src/agent/delegation.ts +390 -36
- package/src/agent/loop.ts +711 -109
- package/src/agent/subagent.ts +59 -0
- package/src/index.ts +154 -59
- package/src/models/catalog-cache.ts +68 -25
- package/src/models/catalog.ts +97 -18
- package/src/providers/google.ts +99 -39
- package/src/providers/openai-compat.ts +140 -80
- package/src/providers/openai.ts +149 -69
- package/src/providers/openrouter.ts +106 -71
- package/src/providers/registry.ts +16 -12
- package/src/providers/shared.ts +132 -0
- package/src/providers.ts +3 -4
- package/src/session/store.ts +512 -33
- package/src/streaming/event-stream.ts +156 -41
- package/src/tools/executor.ts +248 -15
- package/src/tools/tool.ts +147 -3
- package/src/types/agent.ts +229 -2
- package/src/types/errors.ts +133 -0
- package/src/types/model.ts +1 -1
- package/src/types/response.ts +126 -3
- package/src/types/tool.ts +69 -0
- package/src/update-models.ts +8 -2
- package/src/utils/cache.ts +68 -1
- package/src/utils/env.ts +2 -6
- package/src/utils/headers.ts +153 -19
- package/src/utils/retry.ts +27 -13
- package/src/utils/session.ts +284 -0
- package/src/providers/openrouter-responses.ts +0 -949
- package/src/types/provider-payloads.ts +0 -352
|
@@ -55,11 +55,21 @@ import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
|
55
55
|
import { SSEParser } from "../streaming/sse-parser.ts";
|
|
56
56
|
import { AgentResponse } from "../types/response.ts";
|
|
57
57
|
import { getApiKey, getEnv } from "../utils/env.ts";
|
|
58
|
-
import { buildSessionHeaders } from "../utils/headers.ts";
|
|
58
|
+
import { buildSessionHeaders, setHeaderCaseInsensitive, shouldEchoSignature } from "../utils/headers.ts";
|
|
59
59
|
import { normalizeMediaInput } from "../utils/media.ts";
|
|
60
60
|
import { safeStringify } from "../utils/serialization.ts";
|
|
61
61
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
62
62
|
import { withRetries } from "../utils/retry.ts";
|
|
63
|
+
import {
|
|
64
|
+
parseArguments,
|
|
65
|
+
normalizeThinkingParts,
|
|
66
|
+
newToolCallId,
|
|
67
|
+
redactedHeaders,
|
|
68
|
+
readErrorPayload,
|
|
69
|
+
incompleteStreamError,
|
|
70
|
+
combinedSignal,
|
|
71
|
+
timeoutFor,
|
|
72
|
+
} from "./shared.ts";
|
|
63
73
|
import { getModelFromCatalog, createGenericModelSpec } from "../models/catalog.ts";
|
|
64
74
|
import {
|
|
65
75
|
applyCacheForCustom,
|
|
@@ -189,8 +199,6 @@ function resolveBaseUrl(
|
|
|
189
199
|
getEnv(`${envPrefix}_BASE_URL`) ||
|
|
190
200
|
getEnv(`${envPrefix}_BASEURL`) ||
|
|
191
201
|
getEnv(`${envPrefix}_API_BASE`) ||
|
|
192
|
-
getEnv("OPENAI_BASE_URL") ||
|
|
193
|
-
getEnv("OPENAI_API_BASE") ||
|
|
194
202
|
DEFAULT_BASE_URL
|
|
195
203
|
).replace(/\/+$/, "");
|
|
196
204
|
}
|
|
@@ -206,32 +214,36 @@ function resolveApiKey(
|
|
|
206
214
|
configuredApiKey ||
|
|
207
215
|
options?.env?.[`${envPrefix}_API_KEY`] ||
|
|
208
216
|
options?.env?.[`${envPrefix}_BASE_API_KEY`] ||
|
|
209
|
-
options?.env?.["OPENAI_BASE_API_KEY"] ||
|
|
210
|
-
options?.env?.["OPENAI_API_KEY"] ||
|
|
211
217
|
getEnv(`${envPrefix}_API_KEY`) ||
|
|
212
218
|
getEnv(`${envPrefix}_BASE_API_KEY`) ||
|
|
213
|
-
getEnv("OPENAI_BASE_API_KEY") ||
|
|
214
|
-
getEnv("OPENAI_API_KEY") ||
|
|
215
219
|
getApiKey(prefix, undefined, options?.env) ||
|
|
216
220
|
undefined
|
|
217
221
|
);
|
|
218
222
|
}
|
|
219
223
|
|
|
220
|
-
/** Local endpoints (loopback / LAN / *.local) may omit the API key. */
|
|
224
|
+
/** Local endpoints (loopback / LAN / *.local) may omit the API key. Literal IPs only; hostnames like 10.evil.com are not local. */
|
|
221
225
|
function isLocalEndpoint(baseUrl: string): boolean {
|
|
222
226
|
try {
|
|
223
|
-
|
|
227
|
+
let host = new URL(baseUrl).hostname.toLowerCase();
|
|
228
|
+
if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
|
|
224
229
|
if (
|
|
225
230
|
host === "localhost" ||
|
|
226
231
|
host === "127.0.0.1" ||
|
|
227
232
|
host === "0.0.0.0" ||
|
|
228
233
|
host === "::1" ||
|
|
234
|
+
host === "0:0:0:0:0:0:0:1" ||
|
|
229
235
|
host.endsWith(".local")
|
|
230
236
|
) {
|
|
231
237
|
return true;
|
|
232
238
|
}
|
|
233
|
-
if (
|
|
234
|
-
|
|
239
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
|
|
240
|
+
const parts = host.split(".").map(Number);
|
|
241
|
+
const [a, b] = parts as number[];
|
|
242
|
+
if (a === 10) return true;
|
|
243
|
+
if (a === 172 && b !== undefined && b >= 16 && b <= 31) return true;
|
|
244
|
+
if (a === 192 && b === 168) return true;
|
|
245
|
+
if (a === 127) return true;
|
|
246
|
+
return false;
|
|
235
247
|
}
|
|
236
248
|
return false;
|
|
237
249
|
} catch {
|
|
@@ -362,6 +374,10 @@ function resultToOutput(result: unknown): string {
|
|
|
362
374
|
* transport: every turn carries everything). Prior-turn reasoning is NOT
|
|
363
375
|
* resent — except echoed Gemini thought signatures, which strict
|
|
364
376
|
* OpenAI-compatible endpoints require on tool calls they generated.
|
|
377
|
+
*
|
|
378
|
+
* Q-18: signatures are namespaced — only echo when the stored provider is
|
|
379
|
+
* `google` (or untagged legacy). Signatures minted by other providers never
|
|
380
|
+
* leak onto this transport.
|
|
365
381
|
*/
|
|
366
382
|
async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessage[]> {
|
|
367
383
|
const pairing = new Map<string, string>();
|
|
@@ -417,8 +433,9 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
|
|
|
417
433
|
// Echo provider-issued thought signatures verbatim so endpoints
|
|
418
434
|
// that minted them (Gemini behind a compat proxy) keep working.
|
|
419
435
|
// Only present when a previous turn captured one — other endpoints
|
|
420
|
-
// never see this field.
|
|
421
|
-
|
|
436
|
+
// never see this field. Q-18: echo only google/untagged signatures.
|
|
437
|
+
const storedProvider = (part as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
|
|
438
|
+
if (part.thoughtSignature && shouldEchoSignature(storedProvider, "google")) {
|
|
422
439
|
call.extra_content = { google: { thought_signature: part.thoughtSignature } };
|
|
423
440
|
}
|
|
424
441
|
calls.push(call);
|
|
@@ -473,27 +490,6 @@ function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
|
|
|
473
490
|
return usage;
|
|
474
491
|
}
|
|
475
492
|
|
|
476
|
-
function parseArguments(raw: string | undefined): Record<string, unknown> {
|
|
477
|
-
if (!raw) return {};
|
|
478
|
-
try {
|
|
479
|
-
const parsed: unknown = JSON.parse(raw);
|
|
480
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
481
|
-
return parsed as Record<string, unknown>;
|
|
482
|
-
}
|
|
483
|
-
return { raw };
|
|
484
|
-
} catch {
|
|
485
|
-
return { raw };
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/** Trims/collapses assembled thinking parts (no edge-tripling). */
|
|
490
|
-
function normalizeThinkingParts(parts: string[]): string | undefined {
|
|
491
|
-
const cleaned = parts
|
|
492
|
-
.map((p) => p.replace(/\n{3,}/g, "\n\n").trim())
|
|
493
|
-
.filter((p) => p.length > 0);
|
|
494
|
-
return cleaned.length > 0 ? cleaned.join("\n") : undefined;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
493
|
function extractSignature(rawTc: {
|
|
498
494
|
extra_content?: { google?: { thought_signature?: string } };
|
|
499
495
|
}): string | undefined {
|
|
@@ -501,6 +497,12 @@ function extractSignature(rawTc: {
|
|
|
501
497
|
return typeof sig === "string" && sig ? sig : undefined;
|
|
502
498
|
}
|
|
503
499
|
|
|
500
|
+
/** Tags a captured compat signature with its issuing provider (`google`, Q-18). */
|
|
501
|
+
function withCompatSignature(sig: string | undefined): { thoughtSignature?: string; thoughtSignatureProvider?: string } {
|
|
502
|
+
if (!sig) return {};
|
|
503
|
+
return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
|
|
504
|
+
}
|
|
505
|
+
|
|
504
506
|
function throwResponseError(
|
|
505
507
|
response: ChatResponse,
|
|
506
508
|
prefix: string,
|
|
@@ -528,7 +530,9 @@ function parseResponse(
|
|
|
528
530
|
|
|
529
531
|
const choice = response.choices?.[0];
|
|
530
532
|
const message = choice?.message;
|
|
531
|
-
|
|
533
|
+
// Q-23: refusal content surfaces as text with a `refusal` finish reason.
|
|
534
|
+
const refusal = typeof message?.refusal === "string" && message.refusal ? message.refusal : "";
|
|
535
|
+
const text = typeof message?.content === "string" && message.content ? message.content : refusal;
|
|
532
536
|
// Thinking tolerance: plain `reasoning` plus per-block texts (deepseek-style
|
|
533
537
|
// `reasoning_details`). Details win when both ride along; both are
|
|
534
538
|
// display-only here — never resent.
|
|
@@ -547,25 +551,26 @@ function parseResponse(
|
|
|
547
551
|
const args = parseArguments(tc.function?.arguments);
|
|
548
552
|
const sig = extractSignature(tc);
|
|
549
553
|
toolCalls.push({
|
|
550
|
-
id: tc.id ||
|
|
554
|
+
id: tc.id || newToolCallId(),
|
|
551
555
|
name: tc.function?.name || "unknown",
|
|
552
556
|
arguments: args,
|
|
553
557
|
rawArguments:
|
|
554
558
|
typeof tc.function?.arguments === "string"
|
|
555
559
|
? tc.function.arguments
|
|
556
560
|
: JSON.stringify(tc.function?.arguments ?? {}),
|
|
557
|
-
...(sig
|
|
558
|
-
});
|
|
561
|
+
...withCompatSignature(sig),
|
|
562
|
+
} as ToolCallRecord);
|
|
559
563
|
}
|
|
560
564
|
|
|
561
565
|
const wireReason = choice?.finish_reason ?? undefined;
|
|
566
|
+
const finishReason = toolCalls.length > 0 ? "tool_calls" : refusal ? "refusal" : wireReason || "stop";
|
|
562
567
|
return {
|
|
563
568
|
text,
|
|
564
569
|
thinking: normalizeThinkingParts(thinkingParts),
|
|
565
570
|
thoughtSignature: toolCalls.map((c) => c.thoughtSignature).find(Boolean),
|
|
566
571
|
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
567
572
|
usage: mapUsage(response.usage),
|
|
568
|
-
finishReason
|
|
573
|
+
finishReason,
|
|
569
574
|
responseId: response.id,
|
|
570
575
|
model: modelId,
|
|
571
576
|
provider: prefix as ProviderId,
|
|
@@ -574,28 +579,6 @@ function parseResponse(
|
|
|
574
579
|
};
|
|
575
580
|
}
|
|
576
581
|
|
|
577
|
-
function redactedHeaders(headers: Record<string, string>): Record<string, string> {
|
|
578
|
-
const out: Record<string, string> = {};
|
|
579
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
580
|
-
out[k] = k.toLowerCase() === "authorization" ? "[REDACTED]" : v;
|
|
581
|
-
}
|
|
582
|
-
return out;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function readErrorPayload(bodyText: string): { message: string; code?: string | number } {
|
|
586
|
-
try {
|
|
587
|
-
const parsed: unknown = JSON.parse(bodyText);
|
|
588
|
-
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
589
|
-
const err = (first as { error?: { message?: string; code?: string | number } })?.error;
|
|
590
|
-
if (err && typeof err.message === "string") {
|
|
591
|
-
return { message: err.message, code: err.code };
|
|
592
|
-
}
|
|
593
|
-
return { message: bodyText.slice(0, 300) };
|
|
594
|
-
} catch {
|
|
595
|
-
return { message: bodyText.slice(0, 300) };
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
582
|
// ---------------------------------------------------------------------------
|
|
600
583
|
// Provider
|
|
601
584
|
// ---------------------------------------------------------------------------
|
|
@@ -716,6 +699,19 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
716
699
|
}
|
|
717
700
|
// No reasoning / service_tier / session / cache body primitives: strict
|
|
718
701
|
// endpoints reject unknown properties. Affinity is headers-only.
|
|
702
|
+
// Q-38: request usage on streams where supported so terminal accounting
|
|
703
|
+
// arrives on the final chunk. Strict servers that reject unknown fields
|
|
704
|
+
// opt out via `COMPAT_NO_STREAM_OPTIONS=1`; an explicit per-call
|
|
705
|
+
// `(options as any).streamOptions === false` also skips it.
|
|
706
|
+
if (stream) {
|
|
707
|
+
const noStreamOptions =
|
|
708
|
+
(options as { streamOptions?: unknown } | undefined)?.streamOptions === false ||
|
|
709
|
+
getEnv("COMPAT_NO_STREAM_OPTIONS") === "1" ||
|
|
710
|
+
(options?.env?.["COMPAT_NO_STREAM_OPTIONS"] === "1");
|
|
711
|
+
if (!noStreamOptions) {
|
|
712
|
+
(body as unknown as Record<string, unknown>)["stream_options"] = { include_usage: true };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
719
715
|
return body;
|
|
720
716
|
}
|
|
721
717
|
|
|
@@ -735,8 +731,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
735
731
|
for (const [k, v] of Object.entries(headers)) {
|
|
736
732
|
if (INTERNAL_HEADERS.has(k.toLowerCase())) delete headers[k];
|
|
737
733
|
}
|
|
738
|
-
headers
|
|
739
|
-
if (apiKey) headers
|
|
734
|
+
setHeaderCaseInsensitive(headers, "Content-Type", "application/json");
|
|
735
|
+
if (apiKey) setHeaderCaseInsensitive(headers, "Authorization", `Bearer ${apiKey}`);
|
|
740
736
|
return { method: "POST", headers, body: JSON.stringify(body), signal };
|
|
741
737
|
}
|
|
742
738
|
|
|
@@ -748,13 +744,18 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
748
744
|
sessionId: string | undefined,
|
|
749
745
|
signal?: AbortSignal
|
|
750
746
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
headers
|
|
756
|
-
|
|
757
|
-
|
|
747
|
+
const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
|
|
748
|
+
try {
|
|
749
|
+
const res = await fetch(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
750
|
+
const text = await res.text();
|
|
751
|
+
const headers: Record<string, string> = {};
|
|
752
|
+
res.headers.forEach((v, k) => {
|
|
753
|
+
headers[k] = v;
|
|
754
|
+
});
|
|
755
|
+
return { status: res.status, statusText: res.statusText, headers, text };
|
|
756
|
+
} finally {
|
|
757
|
+
cleanup();
|
|
758
|
+
}
|
|
758
759
|
}
|
|
759
760
|
|
|
760
761
|
private throwIfError(
|
|
@@ -764,13 +765,14 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
764
765
|
modelId: string
|
|
765
766
|
): void {
|
|
766
767
|
if (status >= 200 && status < 300) return;
|
|
767
|
-
const { message, code } = readErrorPayload(bodyText);
|
|
768
|
+
const { message, code, errorType } = readErrorPayload(bodyText);
|
|
768
769
|
const err: Record<string, unknown> & Error = new Error(message) as Record<string, unknown> & Error;
|
|
769
770
|
(err as Record<string, unknown>)["statusCode"] = status;
|
|
770
771
|
(err as Record<string, unknown>)["status"] = status;
|
|
771
772
|
(err as Record<string, unknown>)["responseBody"] = bodyText.slice(0, 500);
|
|
772
773
|
(err as Record<string, unknown>)["url"] = url.split("?")[0];
|
|
773
774
|
if (code !== undefined) (err as Record<string, unknown>)["code"] = code;
|
|
775
|
+
if (errorType) (err as Record<string, unknown>)["errorType"] = errorType;
|
|
774
776
|
throw toConciseProviderError(err, this.prefix, modelId);
|
|
775
777
|
}
|
|
776
778
|
|
|
@@ -904,7 +906,21 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
904
906
|
body,
|
|
905
907
|
};
|
|
906
908
|
|
|
907
|
-
const
|
|
909
|
+
const streamTimeoutMs = timeoutFor(options as { timeoutMs?: number } | undefined);
|
|
910
|
+
const streamTimeoutTimer =
|
|
911
|
+
streamTimeoutMs && streamTimeoutMs > 0
|
|
912
|
+
? setTimeout(() => {
|
|
913
|
+
try {
|
|
914
|
+
linked.abort();
|
|
915
|
+
} catch {}
|
|
916
|
+
}, streamTimeoutMs)
|
|
917
|
+
: undefined;
|
|
918
|
+
let res: Response;
|
|
919
|
+
try {
|
|
920
|
+
res = await fetch(url, this.requestInit(body, options, apiKey, sessionId, linked.signal));
|
|
921
|
+
} finally {
|
|
922
|
+
if (streamTimeoutTimer) clearTimeout(streamTimeoutTimer);
|
|
923
|
+
}
|
|
908
924
|
if (!res.ok || !res.body) {
|
|
909
925
|
const text = !res.ok ? await res.text().catch(() => "") : "";
|
|
910
926
|
if (!res.ok) this.throwIfError(res.status, url, text, clean);
|
|
@@ -914,7 +930,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
914
930
|
res.headers.forEach((v, k) => {
|
|
915
931
|
responseHeaders[k] = v;
|
|
916
932
|
});
|
|
917
|
-
const responseMeta = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
933
|
+
const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
918
934
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
919
935
|
|
|
920
936
|
const parser = new SSEParser();
|
|
@@ -928,6 +944,11 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
928
944
|
const signatures = new Map<number, string>();
|
|
929
945
|
let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
930
946
|
let finishReason = "stop";
|
|
947
|
+
let sawFinishReason = false;
|
|
948
|
+
let sawUsage = false;
|
|
949
|
+
let refusalSeen = false;
|
|
950
|
+
let badFrames = 0;
|
|
951
|
+
let framesSeen = 0;
|
|
931
952
|
let responseId: string | undefined;
|
|
932
953
|
let completedBody: unknown = undefined;
|
|
933
954
|
let aborted = false;
|
|
@@ -943,11 +964,17 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
943
964
|
);
|
|
944
965
|
|
|
945
966
|
const handleMessage = (data: string): void => {
|
|
946
|
-
if (!data
|
|
967
|
+
if (!data) return;
|
|
968
|
+
if (data === "[DONE]") {
|
|
969
|
+
sawFinishReason = sawFinishReason || completedBody !== undefined || sawUsage;
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
947
972
|
let msg: Record<string, unknown>;
|
|
948
973
|
try {
|
|
949
974
|
msg = JSON.parse(data) as Record<string, unknown>;
|
|
975
|
+
framesSeen += 1;
|
|
950
976
|
} catch {
|
|
977
|
+
badFrames += 1;
|
|
951
978
|
return;
|
|
952
979
|
}
|
|
953
980
|
// Mid-stream provider error: top-level `error`, HTTP stays 200.
|
|
@@ -969,6 +996,13 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
969
996
|
const choice = (Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
|
|
970
997
|
const delta = (choice["delta"] as Record<string, unknown>) ?? {};
|
|
971
998
|
// Text delta (content-free accounting frames carry "" — ignored).
|
|
999
|
+
// Q-23: refusal deltas surface as text with a `refusal` finish.
|
|
1000
|
+
const deltaRefusal = delta["refusal"];
|
|
1001
|
+
if (typeof deltaRefusal === "string" && deltaRefusal) {
|
|
1002
|
+
text += deltaRefusal;
|
|
1003
|
+
refusalSeen = true;
|
|
1004
|
+
eventStream.push({ type: "text_delta", delta: deltaRefusal, partialText: text });
|
|
1005
|
+
}
|
|
972
1006
|
if (typeof delta["content"] === "string" && delta["content"]) {
|
|
973
1007
|
text += delta["content"] as string;
|
|
974
1008
|
eventStream.push({ type: "text_delta", delta: delta["content"] as string, partialText: text });
|
|
@@ -1042,10 +1076,12 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1042
1076
|
throw toConciseProviderError(failure, prefix, clean);
|
|
1043
1077
|
}
|
|
1044
1078
|
finishReason = fr;
|
|
1079
|
+
sawFinishReason = true;
|
|
1045
1080
|
}
|
|
1046
1081
|
const chunkUsage = msg["usage"] as ChatResponse["usage"] | undefined;
|
|
1047
1082
|
if (chunkUsage) {
|
|
1048
1083
|
usage = mapUsage(chunkUsage);
|
|
1084
|
+
sawUsage = true;
|
|
1049
1085
|
completedBody = completedBody ?? msg;
|
|
1050
1086
|
eventStream.push({ type: "usage", usage });
|
|
1051
1087
|
}
|
|
@@ -1077,20 +1113,41 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1077
1113
|
const args = parseStreamedToolArguments(c.startArgs, c.deltaArgs);
|
|
1078
1114
|
const sig = signatures.get(index);
|
|
1079
1115
|
const record: ToolCallRecord = {
|
|
1080
|
-
id: c.id ||
|
|
1116
|
+
id: c.id || newToolCallId(),
|
|
1081
1117
|
name: c.name,
|
|
1082
1118
|
arguments: args,
|
|
1083
1119
|
rawArguments: c.startArgs + c.deltaArgs,
|
|
1084
|
-
...(sig
|
|
1085
|
-
};
|
|
1120
|
+
...withCompatSignature(sig),
|
|
1121
|
+
} as ToolCallRecord;
|
|
1086
1122
|
toolCalls.push(record);
|
|
1087
1123
|
eventStream.push({ type: "tool_call_complete", toolCall: record });
|
|
1088
1124
|
}
|
|
1089
1125
|
|
|
1090
1126
|
noteProviderTurn(sessionId, prefix);
|
|
1091
|
-
if (toolCalls.length > 0)
|
|
1127
|
+
if (toolCalls.length > 0) {
|
|
1128
|
+
finishReason = "tool_calls";
|
|
1129
|
+
sawFinishReason = true;
|
|
1130
|
+
} else if (refusalSeen) {
|
|
1131
|
+
finishReason = "refusal";
|
|
1132
|
+
sawFinishReason = true;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Q-23: terminal-state guard — fail `incomplete_stream` (retryable)
|
|
1136
|
+
// instead of resolving empty success. Skipped for non-SSE payloads
|
|
1137
|
+
// (JSON mocks) where no SSE frames were ever seen.
|
|
1138
|
+
const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
|
|
1139
|
+
const cleanThinkingEarly = thinking.replace(/\n{3,}/g, "\n\n").trim();
|
|
1140
|
+
const ctEntry = Object.entries(responseHeaders).find(([k]) => k.toLowerCase() === "content-type");
|
|
1141
|
+
const isSSEPayload =
|
|
1142
|
+
(typeof ctEntry?.[1] === "string" && ((ctEntry[1] as string).includes("event-stream") || (ctEntry[1] as string).startsWith("text/"))) ||
|
|
1143
|
+
framesSeen > 0 ||
|
|
1144
|
+
badFrames > 0;
|
|
1145
|
+
if (!hasTerminal && !text && !cleanThinkingEarly && toolCalls.length === 0 && isSSEPayload) {
|
|
1146
|
+
eventStream.fail(incompleteStreamError(prefix, clean));
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1092
1149
|
|
|
1093
|
-
const cleanThinking =
|
|
1150
|
+
const cleanThinking = cleanThinkingEarly;
|
|
1094
1151
|
const finalThoughtSignature = toolCalls.map((c) => c.thoughtSignature).find(Boolean);
|
|
1095
1152
|
const finalResponse = new AgentResponse({
|
|
1096
1153
|
text,
|
|
@@ -1102,7 +1159,10 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1102
1159
|
responseId,
|
|
1103
1160
|
model: clean,
|
|
1104
1161
|
provider: prefix as ProviderId,
|
|
1105
|
-
raw: {
|
|
1162
|
+
raw: {
|
|
1163
|
+
request: rawRequest,
|
|
1164
|
+
response: { ...responseMeta, body: completedBody, badFrames } as unknown as ProviderRawData["response"],
|
|
1165
|
+
},
|
|
1106
1166
|
durationMs: Date.now() - startTime,
|
|
1107
1167
|
});
|
|
1108
1168
|
eventStream.push({ type: "done", delta: "", usage, finishReason, responseId });
|