@bitkyc08/opencodex 2.7.40 → 2.7.41
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/README.md +63 -12
- package/assets/claude-code-models.gif +0 -0
- package/gui/dist/assets/index-B2J4t3te.css +1 -0
- package/gui/dist/assets/index-BmvM6wRb.js +65 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/google.ts +48 -9
- package/src/adapters/kiro-events.ts +15 -3
- package/src/adapters/kiro.ts +292 -28
- package/src/adapters/openai-chat.ts +63 -15
- package/src/adapters/openai-responses.ts +18 -0
- package/src/bridge.ts +76 -21
- package/src/chat/outbound.ts +66 -34
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p.ts +121 -21
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/inbound-debug.ts +4 -0
- package/src/claude/model-info.ts +9 -3
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude-desktop.ts +152 -0
- package/src/cli/claude.ts +37 -3
- package/src/cli/doctor.ts +187 -6
- package/src/cli/help.ts +13 -1
- package/src/cli/index.ts +151 -57
- package/src/cli/status-oauth.ts +68 -0
- package/src/cli/status.ts +4 -0
- package/src/codex/account-lifecycle.ts +31 -0
- package/src/codex/auth-api.ts +133 -26
- package/src/codex/auth-collision.ts +55 -11
- package/src/codex/auth-context.ts +50 -6
- package/src/codex/catalog/provider-fetch.ts +6 -2
- package/src/codex/home.ts +61 -1
- package/src/codex/inject.ts +28 -66
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +39 -3
- package/src/codex/main-account-cache.ts +25 -0
- package/src/codex/model-cache.ts +20 -1
- package/src/codex/paths.ts +5 -0
- package/src/codex/routing.ts +138 -11
- package/src/codex/subagent-model-fallback.ts +455 -0
- package/src/codex/sync.ts +17 -0
- package/src/combos/failover.ts +10 -2
- package/src/combos/index.ts +1 -0
- package/src/combos/types.ts +9 -0
- package/src/config.ts +117 -0
- package/src/grok/inject.ts +339 -0
- package/src/grok/status.ts +88 -0
- package/src/grok/sync.ts +66 -0
- package/src/lib/destination-policy.ts +13 -0
- package/src/lib/errors.ts +59 -0
- package/src/lib/privacy.ts +9 -0
- package/src/lib/process-control.ts +57 -4
- package/src/oauth/health.ts +375 -0
- package/src/oauth/index.ts +69 -22
- package/src/oauth/kiro.ts +5 -3
- package/src/oauth/log.ts +48 -0
- package/src/oauth/store.ts +55 -6
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +143 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/api-keys.ts +5 -5
- package/src/providers/derive.ts +22 -1
- package/src/providers/free-directory.ts +181 -0
- package/src/providers/key-failover.ts +2 -2
- package/src/providers/kiro-models.ts +3 -2
- package/src/providers/openai-tiers.ts +1 -1
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/registry.ts +49 -2
- package/src/responses/parser.ts +49 -20
- package/src/responses/state.ts +156 -2
- package/src/router.ts +73 -5
- package/src/server/chat-completions.ts +78 -22
- package/src/server/claude-messages.ts +8 -0
- package/src/server/images.ts +2 -1
- package/src/server/index.ts +50 -30
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +337 -18
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/combo-routes.ts +3 -3
- package/src/server/management/config-routes.ts +4 -4
- package/src/server/management/logs-usage-routes.ts +64 -4
- package/src/server/management/model-routes.ts +114 -8
- package/src/server/management/oauth-account-routes.ts +31 -5
- package/src/server/management/provider-routes.ts +42 -11
- package/src/server/management/shared.ts +81 -2
- package/src/server/management/system-routes.ts +6 -1
- package/src/server/management-api.ts +24 -7
- package/src/server/port-reclaim.ts +261 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/collaboration.ts +11 -6
- package/src/server/responses/compact.ts +2 -1
- package/src/server/responses/core.ts +358 -155
- package/src/server/responses/passthrough-error.ts +53 -0
- package/src/server/search.ts +2 -1
- package/src/server/system-env.ts +23 -6
- package/src/server/windows-tcp-drop.ts +174 -0
- package/src/service.ts +43 -3
- package/src/types.ts +74 -6
- package/src/update/index.ts +30 -19
- package/src/update/job.ts +30 -15
- package/src/usage/log.ts +158 -2
- package/src/usage/summary.ts +8 -4
- package/src/web-search/loop.ts +4 -1
- package/gui/dist/assets/index-CMip1DzF.css +0 -1
- package/gui/dist/assets/index-cydcmbzC.js +0 -52
|
@@ -2,6 +2,9 @@ import type { ProviderAdapter } from "./base";
|
|
|
2
2
|
import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxUsage } from "../types";
|
|
3
3
|
import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
|
|
4
4
|
import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
|
|
5
|
+
import { debugProviderDiagnostic } from "../lib/debug";
|
|
6
|
+
import { isDebugEnabled } from "../lib/debug-settings";
|
|
7
|
+
import { isCyberPolicyCode } from "../lib/errors";
|
|
5
8
|
import { redactSecretString } from "../lib/redact";
|
|
6
9
|
import { contentPartsToText } from "./image";
|
|
7
10
|
import { neutralizeIdentity } from "./identity";
|
|
@@ -522,6 +525,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
522
525
|
messages,
|
|
523
526
|
stream: parsed.stream,
|
|
524
527
|
};
|
|
528
|
+
if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true;
|
|
525
529
|
const maxTokens = resolveMaxTokens(provider, parsed);
|
|
526
530
|
const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId);
|
|
527
531
|
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
|
|
@@ -545,9 +549,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
545
549
|
const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
|
|
546
550
|
if (budget !== undefined) body.thinking_budget = budget;
|
|
547
551
|
} else if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
|
|
548
|
-
// Vendor thinking-toggle wire
|
|
549
|
-
//
|
|
550
|
-
|
|
552
|
+
// Vendor thinking-toggle wire: the mapped value is sent as `thinking: {type}` because
|
|
553
|
+
// these models ignore/reject reasoning_effort. Most use enabled/disabled; MiniMax-M3
|
|
554
|
+
// uses adaptive/disabled.
|
|
555
|
+
if (reasoningEffort === "enabled" || reasoningEffort === "disabled" || reasoningEffort === "adaptive") {
|
|
551
556
|
body.thinking = { type: reasoningEffort };
|
|
552
557
|
}
|
|
553
558
|
} else {
|
|
@@ -588,7 +593,24 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
588
593
|
if (hasCredential) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
589
594
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
590
595
|
|
|
591
|
-
|
|
596
|
+
const bodyJson = JSON.stringify(body);
|
|
597
|
+
// Never log pathname/query — tenant-scoped hosts (e.g. Cloudflare
|
|
598
|
+
// /accounts/<account_id>/ai/v1) would otherwise leak account identifiers (#452).
|
|
599
|
+
if (isDebugEnabled()) {
|
|
600
|
+
let host = "upstream";
|
|
601
|
+
try { host = new URL(url).host; } catch { /* keep fallback */ }
|
|
602
|
+
debugProviderDiagnostic("openai-chat", "request", {
|
|
603
|
+
host,
|
|
604
|
+
model: body.model,
|
|
605
|
+
stream: parsed.stream,
|
|
606
|
+
messageCount: Array.isArray(messages) ? messages.length : 0,
|
|
607
|
+
toolCount: Array.isArray(tools) ? tools.length : 0,
|
|
608
|
+
hasCredential,
|
|
609
|
+
bodyBytes: new TextEncoder().encode(bodyJson).length,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return { url, method: "POST", headers, body: bodyJson };
|
|
592
614
|
},
|
|
593
615
|
|
|
594
616
|
async *parseStream(response: Response): AsyncGenerator<AdapterEvent> {
|
|
@@ -657,9 +679,21 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
657
679
|
// instead of a clean [DONE]. Surface it as a terminal error so the bridge emits a
|
|
658
680
|
// classified response.failed (bridge case "error") — never a truncated completion.
|
|
659
681
|
if (chunk.error) {
|
|
660
|
-
const err = chunk.error as { message?: string } | undefined;
|
|
682
|
+
const err = chunk.error as { message?: string; code?: string; type?: string; status?: number } | undefined;
|
|
683
|
+
const message = err?.message ?? "upstream error";
|
|
684
|
+
debugProviderDiagnostic("openai-chat", "stream-error", { message });
|
|
661
685
|
yield* flushToolCalls();
|
|
662
|
-
yield {
|
|
686
|
+
yield {
|
|
687
|
+
type: "error",
|
|
688
|
+
message,
|
|
689
|
+
...(typeof err?.code === "string" ? { code: err.code } : {}),
|
|
690
|
+
...(typeof err?.type === "string" ? { errorType: err.type } : {}),
|
|
691
|
+
...(isCyberPolicyCode(err?.code)
|
|
692
|
+
? { status: 400 }
|
|
693
|
+
: typeof err?.status === "number" && Number.isInteger(err.status)
|
|
694
|
+
? { status: err.status }
|
|
695
|
+
: {}),
|
|
696
|
+
};
|
|
663
697
|
return "terminate";
|
|
664
698
|
}
|
|
665
699
|
|
|
@@ -679,13 +713,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
679
713
|
}
|
|
680
714
|
const delta = choices[0].delta;
|
|
681
715
|
if (delta) {
|
|
682
|
-
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
683
|
-
yield { type: "text_delta", text: delta.content };
|
|
684
|
-
}
|
|
685
|
-
|
|
686
716
|
if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
|
|
687
717
|
yield { type: "reasoning_raw_delta", text: delta.reasoning_content };
|
|
688
718
|
}
|
|
719
|
+
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
720
|
+
yield { type: "text_delta", text: delta.content };
|
|
721
|
+
}
|
|
689
722
|
|
|
690
723
|
const toolCalls = delta.tool_calls as { index?: number; id?: string; function?: { name?: string; arguments?: string } }[] | undefined;
|
|
691
724
|
if (toolCalls) {
|
|
@@ -748,6 +781,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
748
781
|
// closed so the bridge emits a classified response.failed rather than a silent truncation.
|
|
749
782
|
const sawFinish = finishReason !== undefined;
|
|
750
783
|
if (!sawFinish && pendingUsage === undefined) {
|
|
784
|
+
debugProviderDiagnostic("openai-chat", "stream-truncated", {
|
|
785
|
+
finishReason: finishReason ?? null,
|
|
786
|
+
hadUsage: false,
|
|
787
|
+
});
|
|
751
788
|
yield { type: "error", message: "upstream stream ended without a terminal signal ([DONE] or finish_reason) — possible truncation" };
|
|
752
789
|
return;
|
|
753
790
|
}
|
|
@@ -766,10 +803,21 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
766
803
|
async parseResponse(response: Response): Promise<AdapterEvent[]> {
|
|
767
804
|
const json = await response.json() as Record<string, unknown>;
|
|
768
805
|
if (json.error) {
|
|
769
|
-
const upstreamError = json.error as { message?: unknown };
|
|
806
|
+
const upstreamError = json.error as { message?: unknown; code?: unknown; type?: unknown; status?: unknown };
|
|
807
|
+
const message = typeof upstreamError.message === "string" ? upstreamError.message : "upstream error";
|
|
808
|
+
const code = typeof upstreamError.code === "string" ? upstreamError.code : undefined;
|
|
809
|
+
const errorType = typeof upstreamError.type === "string" ? upstreamError.type : undefined;
|
|
810
|
+
const status = isCyberPolicyCode(code)
|
|
811
|
+
? 400
|
|
812
|
+
: typeof upstreamError.status === "number" && Number.isInteger(upstreamError.status)
|
|
813
|
+
? upstreamError.status
|
|
814
|
+
: undefined;
|
|
770
815
|
return [{
|
|
771
816
|
type: "error",
|
|
772
|
-
message
|
|
817
|
+
message,
|
|
818
|
+
...(code !== undefined ? { code } : {}),
|
|
819
|
+
...(errorType !== undefined ? { errorType } : {}),
|
|
820
|
+
...(status !== undefined ? { status } : {}),
|
|
773
821
|
}];
|
|
774
822
|
}
|
|
775
823
|
|
|
@@ -780,12 +828,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
780
828
|
}
|
|
781
829
|
|
|
782
830
|
const msg = choices[0].message;
|
|
783
|
-
if (typeof msg.content === "string") {
|
|
784
|
-
events.push({ type: "text_delta", text: msg.content });
|
|
785
|
-
}
|
|
786
831
|
if (typeof msg.reasoning_content === "string" && msg.reasoning_content.length > 0) {
|
|
787
832
|
events.push({ type: "reasoning_raw_delta", text: msg.reasoning_content });
|
|
788
833
|
}
|
|
834
|
+
if (typeof msg.content === "string") {
|
|
835
|
+
events.push({ type: "text_delta", text: msg.content });
|
|
836
|
+
}
|
|
789
837
|
const toolCalls = msg.tool_calls as { id: string; function: { name: string; arguments: string } }[] | undefined;
|
|
790
838
|
if (toolCalls) {
|
|
791
839
|
for (const tc of toolCalls) {
|
|
@@ -461,6 +461,23 @@ function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
|
|
|
461
461
|
return rest;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Remove top-level parameters the ChatGPT backend (`authMode: "forward"`) rejects
|
|
466
|
+
* with `{"detail":"Unsupported parameter: …"}` (strict allowlist). Codex CLI never
|
|
467
|
+
* sends these — it controls output length via `reasoning.effort` — but third-party
|
|
468
|
+
* Responses API clients (GJC, SDK wrappers) include `max_output_tokens` per the
|
|
469
|
+
* public spec. `metadata` is likewise absent from the allowlist. No-op when the
|
|
470
|
+
* body carries neither field, keeping the common Codex path allocation-free.
|
|
471
|
+
*/
|
|
472
|
+
function stripUnsupportedForwardParams(body: unknown): unknown {
|
|
473
|
+
if (!isPlainObject(body)) return body;
|
|
474
|
+
const hasMot = Object.prototype.hasOwnProperty.call(body, "max_output_tokens");
|
|
475
|
+
const hasMeta = Object.prototype.hasOwnProperty.call(body, "metadata");
|
|
476
|
+
if (!hasMot && !hasMeta) return body;
|
|
477
|
+
const { max_output_tokens: _mot, metadata: _meta, ...rest } = body;
|
|
478
|
+
return rest;
|
|
479
|
+
}
|
|
480
|
+
|
|
464
481
|
/**
|
|
465
482
|
* Hosted tool types whose server-side function names collide with the client tools Codex
|
|
466
483
|
* declares for the matching app skill. Codex sends BOTH (e.g. hosted `image_generation` plus a
|
|
@@ -644,6 +661,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
644
661
|
);
|
|
645
662
|
if (forward) {
|
|
646
663
|
outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
|
|
664
|
+
outBody = stripUnsupportedForwardParams(outBody);
|
|
647
665
|
}
|
|
648
666
|
else outBody = stripConflictingHostedTools(outBody);
|
|
649
667
|
if (forward || parsed._previousResponseInputExpanded === true) {
|
package/src/bridge.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AdapterEvent, OcxMessagePhase, OcxProviderContinuationState, OcxUsage } from "./types";
|
|
2
|
-
import { adapterFailureFromMessage, classifyError, type OcxErrorPayload } from "./lib/errors";
|
|
2
|
+
import { adapterFailureFromMessage, classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, type OcxErrorPayload } from "./lib/errors";
|
|
3
3
|
import { encodeCompactionSummary } from "./responses/compaction";
|
|
4
4
|
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
|
|
5
5
|
import { resolveStallTimeoutSec } from "./stall-timeout";
|
|
@@ -14,28 +14,48 @@ function sseEvent(name: string, data: Record<string, unknown>): string {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
17
|
+
// input_tokens_details / output_tokens_details are ALWAYS emitted (zero defaults):
|
|
18
|
+
// strict Responses clients deserialize them as required fields — grok-build's pinned
|
|
19
|
+
// async-openai fork (rev 95b52ebd, response_usage.rs) has non-Option InputTokenDetails/
|
|
20
|
+
// OutputTokenDetails, so omitting them turns a successful turn into a hard exit after
|
|
21
|
+
// response.completed ("missing field `input_tokens_details`", verified live 2026-07-23).
|
|
22
|
+
if (!usage) {
|
|
23
|
+
return {
|
|
24
|
+
input_tokens: 0,
|
|
25
|
+
output_tokens: 0,
|
|
26
|
+
total_tokens: 0,
|
|
27
|
+
input_tokens_details: { cached_tokens: 0 },
|
|
28
|
+
output_tokens_details: { reasoning_tokens: 0 },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// inputTokens is already inclusive of cache read/write (types.ts convention). Stateful
|
|
32
|
+
// providers may report an absolute active-context checkpoint separately from their
|
|
33
|
+
// per-attempt usage. Split that checkpoint into input + output without adding output twice.
|
|
34
|
+
const inputTokens = usage.contextTotalTokens !== undefined
|
|
35
|
+
? Math.max(0, usage.contextTotalTokens - usage.outputTokens)
|
|
36
|
+
: usage.inputTokens;
|
|
20
37
|
const out: Record<string, unknown> = {
|
|
21
38
|
input_tokens: inputTokens,
|
|
22
39
|
output_tokens: usage.outputTokens,
|
|
23
|
-
total_tokens:
|
|
40
|
+
total_tokens: usage.contextTotalTokens !== undefined
|
|
41
|
+
? usage.contextTotalTokens
|
|
42
|
+
: usageDisplayTotalTokens(usage) ?? inputTokens + usage.outputTokens,
|
|
43
|
+
};
|
|
44
|
+
// cached_tokens carries cache READS only, matching OpenAI semantics, and is always present
|
|
45
|
+
// (zero default) for strict clients. Clamp to inputTokens so a provider's absolute
|
|
46
|
+
// checkpoint can never report more cache reads than input.
|
|
47
|
+
const inputDetails: Record<string, number> = {
|
|
48
|
+
cached_tokens: Math.min(usage.cachedInputTokens ?? 0, inputTokens),
|
|
24
49
|
};
|
|
25
|
-
const inputDetails: Record<string, number> = {};
|
|
26
|
-
if (usage.cachedInputTokens !== undefined) {
|
|
27
|
-
// cached_tokens carries cache READS only, matching OpenAI semantics.
|
|
28
|
-
inputDetails.cached_tokens = usage.cachedInputTokens;
|
|
29
|
-
}
|
|
30
50
|
if (usage.cacheCreationInputTokens !== undefined) {
|
|
31
|
-
inputDetails.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (usage.reasoningOutputTokens !== undefined) {
|
|
37
|
-
out.output_tokens_details = { reasoning_tokens: usage.reasoningOutputTokens };
|
|
51
|
+
const cacheRead = inputDetails.cached_tokens ?? 0;
|
|
52
|
+
inputDetails.cache_write_tokens = Math.min(
|
|
53
|
+
usage.cacheCreationInputTokens,
|
|
54
|
+
Math.max(0, inputTokens - cacheRead),
|
|
55
|
+
);
|
|
38
56
|
}
|
|
57
|
+
out.input_tokens_details = inputDetails;
|
|
58
|
+
out.output_tokens_details = { reasoning_tokens: usage.reasoningOutputTokens ?? 0 };
|
|
39
59
|
return out;
|
|
40
60
|
}
|
|
41
61
|
|
|
@@ -48,10 +68,16 @@ function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "error" }>
|
|
|
48
68
|
return adapterFailureFromMessage(event.message);
|
|
49
69
|
}
|
|
50
70
|
const fallback = adapterFailureFromMessage(event.message);
|
|
51
|
-
|
|
71
|
+
let httpStatus = event.status ?? fallback.httpStatus;
|
|
52
72
|
const error = classifyError(httpStatus, event.errorType ?? fallback.error.type, event.message);
|
|
53
73
|
if (event.errorType !== undefined) error.type = event.errorType;
|
|
54
74
|
if (event.code !== undefined) error.code = event.code;
|
|
75
|
+
// Codex maps cyber_policy on HTTP 400 (body) or mid-stream code; never leave it as 502.
|
|
76
|
+
if (isCyberPolicyCode(error.code) || isCyberPolicyCode(event.code)) {
|
|
77
|
+
error.code = CYBER_POLICY_ERROR_CODE;
|
|
78
|
+
error.type = "invalid_request_error";
|
|
79
|
+
httpStatus = 400;
|
|
80
|
+
}
|
|
55
81
|
return { httpStatus, error };
|
|
56
82
|
}
|
|
57
83
|
|
|
@@ -98,6 +124,15 @@ export function bridgeToResponsesSSE(
|
|
|
98
124
|
onFirstOutput?: () => void;
|
|
99
125
|
onTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
100
126
|
onCompletedResponse?: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
|
|
127
|
+
/**
|
|
128
|
+
* Raw adapter-reported usage at the terminal event, BEFORE wire normalization.
|
|
129
|
+
* responsesUsage() always emits token-detail objects with zero defaults for strict
|
|
130
|
+
* clients (grok-build), which makes the wire unusable as a provenance source: the
|
|
131
|
+
* request log must not read synthetic zeros as measured cache/reasoning numbers
|
|
132
|
+
* (cache_detail_missing would be silently suppressed). Callers set logCtx.usage
|
|
133
|
+
* from this callback instead of re-parsing the bridged SSE.
|
|
134
|
+
*/
|
|
135
|
+
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
101
136
|
},
|
|
102
137
|
): ReadableStream<Uint8Array> {
|
|
103
138
|
// Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
|
|
@@ -672,11 +707,13 @@ export function bridgeToResponsesSSE(
|
|
|
672
707
|
// Cache max-output partials so previous_response_id replay can continue them;
|
|
673
708
|
// rememberResponseState rejects content-filtered incomplete responses.
|
|
674
709
|
options?.onCompletedResponse?.(response, event.providerState);
|
|
710
|
+
options?.onUsage?.(event.usage);
|
|
675
711
|
emit("response.incomplete", { response });
|
|
676
712
|
reportTerminal("incomplete");
|
|
677
713
|
} else {
|
|
678
714
|
const response = { ...responseSnapshot("completed", finishedItems, event.endTurn), usage: responsesUsage(event.usage) };
|
|
679
715
|
options?.onCompletedResponse?.(response, event.providerState);
|
|
716
|
+
options?.onUsage?.(event.usage);
|
|
680
717
|
emit("response.completed", {
|
|
681
718
|
response,
|
|
682
719
|
});
|
|
@@ -693,6 +730,7 @@ export function bridgeToResponsesSSE(
|
|
|
693
730
|
if (currentToolCall) closeCurrentToolCall();
|
|
694
731
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
695
732
|
flushHiddenReasoningEnvelope();
|
|
733
|
+
options?.onUsage?.(event.usage);
|
|
696
734
|
emit("response.incomplete", {
|
|
697
735
|
response: {
|
|
698
736
|
...responseSnapshot("incomplete", finishedItems, event.endTurn),
|
|
@@ -716,6 +754,7 @@ export function bridgeToResponsesSSE(
|
|
|
716
754
|
if (currentToolCall) closeCurrentToolCall();
|
|
717
755
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
718
756
|
const failure = adapterFailureFromEvent(event);
|
|
757
|
+
if (event.usage) options?.onUsage?.(event.usage);
|
|
719
758
|
emit("response.failed", {
|
|
720
759
|
response: {
|
|
721
760
|
...responseSnapshot("failed", finishedItems),
|
|
@@ -773,6 +812,7 @@ export function bridgeToResponsesSSE(
|
|
|
773
812
|
flushHiddenRawReasoning();
|
|
774
813
|
if (currentToolCall) closeCurrentToolCall();
|
|
775
814
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
815
|
+
options?.onUsage?.(undefined);
|
|
776
816
|
emit("response.incomplete", {
|
|
777
817
|
response: {
|
|
778
818
|
...responseSnapshot("incomplete", finishedItems),
|
|
@@ -867,6 +907,8 @@ export function buildResponseJSON(
|
|
|
867
907
|
/** Remote compaction v2 turn — append one synthetic compaction output item (see bridgeToResponsesSSE). */
|
|
868
908
|
compaction?: boolean;
|
|
869
909
|
onProviderState?: (state: OcxProviderContinuationState) => void;
|
|
910
|
+
/** Raw adapter-reported usage before wire normalization (see bridgeToResponsesSSE onUsage). */
|
|
911
|
+
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
870
912
|
},
|
|
871
913
|
): Record<string, unknown> {
|
|
872
914
|
const responseId = `resp_${uuid()}`;
|
|
@@ -1091,6 +1133,7 @@ export function buildResponseJSON(
|
|
|
1091
1133
|
: incompleteEvent || stopReason === "max_tokens"
|
|
1092
1134
|
? "incomplete"
|
|
1093
1135
|
: "completed";
|
|
1136
|
+
options?.onUsage?.(incompleteEvent?.usage ?? usage);
|
|
1094
1137
|
return {
|
|
1095
1138
|
id: responseId, object: "response",
|
|
1096
1139
|
created_at: Math.floor(Date.now() / 1000),
|
|
@@ -1112,8 +1155,20 @@ export function buildResponseJSON(
|
|
|
1112
1155
|
};
|
|
1113
1156
|
}
|
|
1114
1157
|
|
|
1115
|
-
export function formatErrorResponse(
|
|
1116
|
-
|
|
1117
|
-
|
|
1158
|
+
export function formatErrorResponse(
|
|
1159
|
+
status: number,
|
|
1160
|
+
type: string,
|
|
1161
|
+
message: string,
|
|
1162
|
+
options?: { code?: string | null },
|
|
1163
|
+
): Response {
|
|
1164
|
+
const error = classifyError(status, type, message);
|
|
1165
|
+
if (isCyberPolicyCode(options?.code)) {
|
|
1166
|
+
error.code = CYBER_POLICY_ERROR_CODE;
|
|
1167
|
+
error.type = "invalid_request_error";
|
|
1168
|
+
}
|
|
1169
|
+
const finalStatus = error.code === CYBER_POLICY_ERROR_CODE ? 400 : status;
|
|
1170
|
+
return new Response(JSON.stringify({ error }), {
|
|
1171
|
+
status: finalStatus,
|
|
1172
|
+
headers: { "Content-Type": "application/json" },
|
|
1118
1173
|
});
|
|
1119
1174
|
}
|
package/src/chat/outbound.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
type Rec = Record<string, unknown>;
|
|
9
9
|
|
|
10
10
|
import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
11
|
+
import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, isCyberPolicyMessage } from "../lib/errors";
|
|
11
12
|
|
|
12
13
|
function isRec(v: unknown): v is Rec {
|
|
13
14
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
@@ -27,39 +28,64 @@ export function chatCompletionsUsage(usage: unknown): Rec {
|
|
|
27
28
|
const details = isRec(u.input_tokens_details) ? u.input_tokens_details : {};
|
|
28
29
|
const prompt = typeof u.input_tokens === "number" ? u.input_tokens : 0;
|
|
29
30
|
const completion = typeof u.output_tokens === "number" ? u.output_tokens : 0;
|
|
30
|
-
const cached = typeof details.cached_tokens === "number" ? details.cached_tokens :
|
|
31
|
+
const cached = typeof details.cached_tokens === "number" ? details.cached_tokens : 0;
|
|
31
32
|
const out: Rec = {
|
|
32
33
|
prompt_tokens: prompt,
|
|
33
34
|
completion_tokens: completion,
|
|
34
35
|
total_tokens: prompt + completion,
|
|
35
36
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Detail objects are always emitted (zero defaults) so strict OpenAI-compatible
|
|
38
|
+
// clients that require them (see responsesUsage in src/bridge.ts) never fail on
|
|
39
|
+
// routed providers that report no cache/reasoning numbers.
|
|
40
|
+
out.prompt_tokens_details = { cached_tokens: cached };
|
|
39
41
|
const outDetails = isRec(u.output_tokens_details) ? u.output_tokens_details : {};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
42
|
+
const reasoning = typeof outDetails.reasoning_tokens === "number" ? outDetails.reasoning_tokens : 0;
|
|
43
|
+
out.completion_tokens_details = { reasoning_tokens: reasoning };
|
|
43
44
|
return out;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
export function chatCompletionsErrorBody(
|
|
47
|
+
export function chatCompletionsErrorBody(
|
|
48
|
+
status: number,
|
|
49
|
+
message: string,
|
|
50
|
+
type = "invalid_request_error",
|
|
51
|
+
code?: string | null,
|
|
52
|
+
): Rec {
|
|
53
|
+
if (isCyberPolicyCode(code) || isCyberPolicyMessage(message)) {
|
|
54
|
+
return {
|
|
55
|
+
error: {
|
|
56
|
+
message,
|
|
57
|
+
type: "invalid_request_error",
|
|
58
|
+
param: null,
|
|
59
|
+
code: CYBER_POLICY_ERROR_CODE,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
47
63
|
return {
|
|
48
64
|
error: {
|
|
49
65
|
message,
|
|
50
66
|
type,
|
|
51
67
|
param: null,
|
|
52
|
-
code:
|
|
53
|
-
|
|
54
|
-
: status ===
|
|
55
|
-
|
|
68
|
+
code: code !== undefined
|
|
69
|
+
? code
|
|
70
|
+
: status === 401 ? "invalid_api_key"
|
|
71
|
+
: status === 404 ? "model_not_found"
|
|
72
|
+
: status === 429 ? "rate_limit_exceeded"
|
|
73
|
+
: null,
|
|
56
74
|
},
|
|
57
75
|
};
|
|
58
76
|
}
|
|
59
77
|
|
|
60
|
-
export function chatCompletionsErrorResponse(
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
export function chatCompletionsErrorResponse(
|
|
79
|
+
status: number,
|
|
80
|
+
message: string,
|
|
81
|
+
type?: string,
|
|
82
|
+
code?: string | null,
|
|
83
|
+
): Response {
|
|
84
|
+
const body = chatCompletionsErrorBody(status, message, type, code);
|
|
85
|
+
const err = body.error as { code?: string | null };
|
|
86
|
+
const finalStatus = err.code === CYBER_POLICY_ERROR_CODE ? 400 : status;
|
|
87
|
+
return new Response(JSON.stringify(body), {
|
|
88
|
+
status: finalStatus,
|
|
63
89
|
headers: { "Content-Type": "application/json" },
|
|
64
90
|
});
|
|
65
91
|
}
|
|
@@ -85,6 +111,7 @@ export function isChatCompletionsStreamError(err: unknown): err is ChatCompletio
|
|
|
85
111
|
|
|
86
112
|
function streamErrorStatus(message: string): number {
|
|
87
113
|
const lower = message.toLowerCase();
|
|
114
|
+
if (isCyberPolicyMessage(lower)) return 400;
|
|
88
115
|
if (lower.includes("truncated")) return 502;
|
|
89
116
|
if (lower.includes("rate") || lower.includes("429")) return 429;
|
|
90
117
|
if (lower.includes("unauthor") || lower.includes("401") || lower.includes("api key")) return 401;
|
|
@@ -93,13 +120,6 @@ function streamErrorStatus(message: string): number {
|
|
|
93
120
|
return 502;
|
|
94
121
|
}
|
|
95
122
|
|
|
96
|
-
function streamErrorType(status: number): string {
|
|
97
|
-
if (status === 401) return "authentication_error";
|
|
98
|
-
if (status === 429) return "rate_limit_error";
|
|
99
|
-
if (status >= 500) return "server_error";
|
|
100
|
-
return "invalid_request_error";
|
|
101
|
-
}
|
|
102
|
-
|
|
103
123
|
function dataFrame(payload: Rec | "[DONE]"): string {
|
|
104
124
|
if (payload === "[DONE]") return "data: [DONE]\n\n";
|
|
105
125
|
return `data: ${JSON.stringify(payload)}\n\n`;
|
|
@@ -220,7 +240,7 @@ export function responsesSseToChatCompletionsSse(
|
|
|
220
240
|
emit(frame);
|
|
221
241
|
emit("[DONE]");
|
|
222
242
|
};
|
|
223
|
-
const fail = (message: string) => {
|
|
243
|
+
const fail = (message: string, details?: { code?: string | null; type?: string; status?: number }) => {
|
|
224
244
|
if (terminated) return;
|
|
225
245
|
terminated = true;
|
|
226
246
|
failed = true;
|
|
@@ -229,18 +249,21 @@ export function responsesSseToChatCompletionsSse(
|
|
|
229
249
|
// Deliver the error frame then close the stream abnormally (no [DONE]).
|
|
230
250
|
// Do not controller.error() — that can drop already-enqueued bytes from consumers
|
|
231
251
|
// like response.text().
|
|
232
|
-
const
|
|
233
|
-
const
|
|
252
|
+
const statusHint = details?.status ?? streamErrorStatus(message);
|
|
253
|
+
const classified = classifyError(statusHint, details?.type ?? "upstream_error", message);
|
|
254
|
+
if (isCyberPolicyCode(details?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
255
|
+
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
256
|
+
classified.type = "invalid_request_error";
|
|
257
|
+
} else if (details?.code !== undefined && details.code !== null && !classified.code) {
|
|
258
|
+
classified.code = details.code;
|
|
259
|
+
}
|
|
234
260
|
try {
|
|
235
261
|
controller.enqueue(encoder.encode(dataFrame({
|
|
236
262
|
error: {
|
|
237
|
-
message,
|
|
238
|
-
type,
|
|
263
|
+
message: classified.message,
|
|
264
|
+
type: classified.type,
|
|
239
265
|
param: null,
|
|
240
|
-
code:
|
|
241
|
-
: status === 404 ? "model_not_found"
|
|
242
|
-
: status === 429 ? "rate_limit_exceeded"
|
|
243
|
-
: null,
|
|
266
|
+
code: classified.code,
|
|
244
267
|
},
|
|
245
268
|
})));
|
|
246
269
|
emittedFrames++;
|
|
@@ -373,7 +396,13 @@ export function responsesSseToChatCompletionsSse(
|
|
|
373
396
|
const response = isRec(data.response) ? data.response : {};
|
|
374
397
|
const error = isRec(response.error) ? response.error : {};
|
|
375
398
|
const message = typeof error.message === "string" ? error.message : "upstream request failed";
|
|
376
|
-
|
|
399
|
+
const code = typeof error.code === "string" ? error.code : null;
|
|
400
|
+
const type = typeof error.type === "string" ? error.type : undefined;
|
|
401
|
+
fail(message, {
|
|
402
|
+
code,
|
|
403
|
+
type,
|
|
404
|
+
...(code === CYBER_POLICY_ERROR_CODE ? { status: 400 } : {}),
|
|
405
|
+
});
|
|
377
406
|
break;
|
|
378
407
|
}
|
|
379
408
|
default:
|
|
@@ -555,8 +584,11 @@ export async function collectChatCompletion(
|
|
|
555
584
|
? parsed.error.message
|
|
556
585
|
: "upstream request failed";
|
|
557
586
|
const type = typeof parsed.error.type === "string" ? parsed.error.type : "server_error";
|
|
558
|
-
const
|
|
559
|
-
|
|
587
|
+
const code = typeof parsed.error.code === "string" ? parsed.error.code : null;
|
|
588
|
+
const status = code === CYBER_POLICY_ERROR_CODE || isCyberPolicyMessage(message)
|
|
589
|
+
? 400
|
|
590
|
+
: streamErrorStatus(message);
|
|
591
|
+
streamError = new ChatCompletionsStreamError(message, { status, type, code });
|
|
560
592
|
continue;
|
|
561
593
|
}
|
|
562
594
|
if (parsed.usage) usage = parsed.usage;
|