@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.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/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { isNativeOpenAIChatTarget, stripBracketedModelSuffix } from "./wire";
|
|
2
|
+
import { reasoningDetailSegmentForWire } from "./response-events";
|
|
3
|
+
import { isVolcengineArkPaygChatTarget } from "./tool-schema";
|
|
4
|
+
import { contentPartsToText } from "../image";
|
|
5
|
+
import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../empty-tool-output-annotation";
|
|
6
|
+
import { identifyRoutedModel } from "../identity";
|
|
7
|
+
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "../tool-catalog-nudge";
|
|
8
|
+
import { registryEntryForProviderDestination } from "../../providers/registry";
|
|
9
|
+
import { peekReasoningForCall } from "../../responses/reasoning-replay-cache";
|
|
10
|
+
import type { OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall } from "../../types";
|
|
11
|
+
import { modelInList, namespacedToolName } from "../../types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The translated Chat route has no video mapping: this adapter does not implement one,
|
|
15
|
+
* and the marker records that fact so the payload is not dropped in silence.
|
|
16
|
+
*
|
|
17
|
+
* The wording is deliberately about opencodex's own translation, not the provider or
|
|
18
|
+
* model. An earlier revision said "unsupported by this provider", which attributed an
|
|
19
|
+
* opencodex mapping limit to upstream capability the proxy has not established. Native
|
|
20
|
+
* Chat passthrough and Google inline video are unaffected by this route.
|
|
21
|
+
*/
|
|
22
|
+
const VIDEO_UNSUPPORTED_MARKER = "[video omitted: the translated Chat route has no video mapping]";
|
|
23
|
+
|
|
24
|
+
export function developerSystemText(message: OcxMessage): string | undefined {
|
|
25
|
+
if (message.role !== "developer") return undefined;
|
|
26
|
+
if (typeof message.content === "string") return message.content;
|
|
27
|
+
if (message.content.some(part => part.type === "image")) return undefined;
|
|
28
|
+
return message.content.map(part => (part as OcxTextContent).text).join("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Chat-completions image_url parts for images carried inside a tool result (issue #888). role:"tool"
|
|
33
|
+
* content is text-only on every chat provider, so these ride in a follow-up user message instead of
|
|
34
|
+
* being flattened to the "[image]" marker the model can't actually see. Data URLs and remote https
|
|
35
|
+
* URLs are both valid in image_url.url, unlike Gemini inline_data which needs base64.
|
|
36
|
+
*/
|
|
37
|
+
export function toolResultTextForWire(content: string | OcxContentPart[], annotateEmpty = false): string {
|
|
38
|
+
// An empty content array is a present-but-empty result; `contentPartsToText` would
|
|
39
|
+
// otherwise fall back to the "[image]" marker and hide the emptiness from the model.
|
|
40
|
+
if (annotateEmpty && Array.isArray(content) && content.length === 0) return EMPTY_TOOL_OUTPUT_ANNOTATION;
|
|
41
|
+
if (typeof content === "string") {
|
|
42
|
+
if (annotateEmpty && content.trim() === "") return EMPTY_TOOL_OUTPUT_ANNOTATION;
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
const text = content.filter((p) => p.type === "text").map((p) => (p as OcxTextContent).text).join("");
|
|
46
|
+
// A whitespace-only text-part array is the array twin of a blank string; the
|
|
47
|
+
// shared emptiness contract (same module as the Responses adapter) annotates it
|
|
48
|
+
// instead of forwarding whitespace the model silently accepts. Image parts and
|
|
49
|
+
// any other non-text part keep the array non-empty.
|
|
50
|
+
if (annotateEmpty && isWhitespaceOnlyTextPartArray(content)) {
|
|
51
|
+
return EMPTY_TOOL_OUTPUT_ANNOTATION;
|
|
52
|
+
}
|
|
53
|
+
if (text) {
|
|
54
|
+
const untransportableImages = content.filter((p) => p.type === "image" && !p.imageUrl).length;
|
|
55
|
+
return `${text}${"[image]".repeat(untransportableImages)}`;
|
|
56
|
+
}
|
|
57
|
+
return contentPartsToText(content);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function toolResultImageChatParts(content: string | OcxContentPart[]): unknown[] {
|
|
61
|
+
if (typeof content === "string") return [];
|
|
62
|
+
const parts: unknown[] = [];
|
|
63
|
+
for (const p of content) {
|
|
64
|
+
if (p.type !== "image" || !p.imageUrl) continue;
|
|
65
|
+
parts.push({ type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } });
|
|
66
|
+
}
|
|
67
|
+
return parts;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
|
|
71
|
+
const out: unknown[] = [];
|
|
72
|
+
const { context, options } = parsed;
|
|
73
|
+
const replayCacheScope = parsed._reasoningReplayScope;
|
|
74
|
+
|
|
75
|
+
interface PendingToolCall { id: string; name: string }
|
|
76
|
+
let pendingToolCalls: PendingToolCall[] = [];
|
|
77
|
+
let deferredBarrierMessages: unknown[] = [];
|
|
78
|
+
let pendingToolResultImageParts: unknown[] = [];
|
|
79
|
+
let mintedIdSeq = 0;
|
|
80
|
+
const seenWireCallIds = new Set<string>();
|
|
81
|
+
|
|
82
|
+
const mintCallId = (): string => {
|
|
83
|
+
let id = "";
|
|
84
|
+
do {
|
|
85
|
+
id = `call_ocx_minted_${++mintedIdSeq}`;
|
|
86
|
+
} while (seenWireCallIds.has(id));
|
|
87
|
+
seenWireCallIds.add(id);
|
|
88
|
+
return id;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const releaseDeferredBarriers = (): void => {
|
|
92
|
+
if (deferredBarrierMessages.length === 0) return;
|
|
93
|
+
out.push(...deferredBarrierMessages);
|
|
94
|
+
deferredBarrierMessages = [];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const flushToolResultImages = (): void => {
|
|
98
|
+
if (pendingToolResultImageParts.length === 0) return;
|
|
99
|
+
out.push({
|
|
100
|
+
role: "user",
|
|
101
|
+
content: [
|
|
102
|
+
{ type: "text", text: "[ocx] image output from the preceding tool result(s):" },
|
|
103
|
+
...pendingToolResultImageParts,
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
pendingToolResultImageParts = [];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const flushPendingToolCalls = (): void => {
|
|
110
|
+
if (pendingToolCalls.length === 0) return;
|
|
111
|
+
for (const call of pendingToolCalls) {
|
|
112
|
+
out.push({
|
|
113
|
+
role: "tool",
|
|
114
|
+
tool_call_id: call.id,
|
|
115
|
+
content: `[ocx] no tool result was recorded for "${call.name}"; execution status unknown — do not treat this as success, failure, or user-provided input.`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
pendingToolCalls = [];
|
|
119
|
+
flushToolResultImages();
|
|
120
|
+
releaseDeferredBarriers();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const nativeOpenAI = isNativeOpenAIChatTarget(provider);
|
|
124
|
+
// Hoisting a newly appended reminder rewrites the reusable prompt prefix.
|
|
125
|
+
// Keep this compatibility exception on the destination/model tested with OCG.
|
|
126
|
+
const chronologicalSystem = parsed.modelId === "deepseek-v4.1-flash"
|
|
127
|
+
&& registryEntryForProviderDestination(provider)?.id === "opencode-go";
|
|
128
|
+
const toolCatalogNudge = shouldInjectNonOpenAIToolCatalogNudge(provider)
|
|
129
|
+
? buildNonOpenAIToolCatalogNudgeForTools(context.tools, options.toolChoice)
|
|
130
|
+
: undefined;
|
|
131
|
+
const developerSystemParts = nativeOpenAI || chronologicalSystem
|
|
132
|
+
? []
|
|
133
|
+
: context.messages
|
|
134
|
+
.map(developerSystemText)
|
|
135
|
+
.filter((part): part is string => part !== undefined && part.length > 0);
|
|
136
|
+
const systemParts = [
|
|
137
|
+
...(context.systemPrompt ?? []),
|
|
138
|
+
...developerSystemParts,
|
|
139
|
+
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
140
|
+
];
|
|
141
|
+
if (systemParts.length > 0) {
|
|
142
|
+
const wireModelId = provider.modelSuffixBracketStrip
|
|
143
|
+
? stripBracketedModelSuffix(parsed.modelId)
|
|
144
|
+
: parsed.modelId;
|
|
145
|
+
const sys = identifyRoutedModel(systemParts.join("\n\n"), wireModelId);
|
|
146
|
+
out.push({ role: "system", content: sys });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const msg of context.messages) {
|
|
150
|
+
switch (msg.role) {
|
|
151
|
+
case "user":
|
|
152
|
+
case "developer": {
|
|
153
|
+
const parts = typeof msg.content === "string" ? undefined : msg.content as OcxContentPart[];
|
|
154
|
+
const hasImages = parts?.some(p => p.type === "image") ?? false;
|
|
155
|
+
let chatMsg: Record<string, unknown>;
|
|
156
|
+
if (msg.role === "developer" && !hasImages) {
|
|
157
|
+
if (!nativeOpenAI && !chronologicalSystem) break;
|
|
158
|
+
const text = typeof msg.content === "string"
|
|
159
|
+
? msg.content
|
|
160
|
+
: parts!.map(p => (p as OcxTextContent).text).join("");
|
|
161
|
+
// A non-text timeline part (video, for example) serializes to nothing here.
|
|
162
|
+
// The generic path drops such a message; the chronological exception must not
|
|
163
|
+
// turn it into an empty system message that some upstreams reject.
|
|
164
|
+
if (!nativeOpenAI && text.length === 0) break;
|
|
165
|
+
chatMsg = { role: nativeOpenAI ? "developer" : "system", content: text };
|
|
166
|
+
} else if (typeof msg.content === "string") {
|
|
167
|
+
chatMsg = { role: "user", content: msg.content };
|
|
168
|
+
} else if (!hasImages) {
|
|
169
|
+
// A video part has no `text`, so joining it produced "" and the whole message
|
|
170
|
+
// was dropped: a video-only or text-plus-video turn vanished silently. OpenAI's
|
|
171
|
+
// Chat Completions wire has no video content part, so state the omission
|
|
172
|
+
// instead of losing it. Scoped to this adapter's wire, not a claim about video
|
|
173
|
+
// support in general — native Chat passthrough and Google inline video are
|
|
174
|
+
// unaffected.
|
|
175
|
+
chatMsg = {
|
|
176
|
+
role: "user",
|
|
177
|
+
content: parts!.map(p => (p.type === "video"
|
|
178
|
+
? VIDEO_UNSUPPORTED_MARKER
|
|
179
|
+
: (p as OcxTextContent).text)).join(""),
|
|
180
|
+
};
|
|
181
|
+
} else {
|
|
182
|
+
const chatParts = parts!.map(p => {
|
|
183
|
+
if (p.type === "image") {
|
|
184
|
+
return { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } };
|
|
185
|
+
}
|
|
186
|
+
// Previously this produced { type: "text", text: undefined } for a video
|
|
187
|
+
// part — a malformed part, worse than a drop because it can fail upstream
|
|
188
|
+
// schema validation.
|
|
189
|
+
if (p.type === "video") return { type: "text", text: VIDEO_UNSUPPORTED_MARKER };
|
|
190
|
+
return { type: "text", text: (p as OcxTextContent).text };
|
|
191
|
+
});
|
|
192
|
+
chatMsg = { role: "user", content: chatParts };
|
|
193
|
+
}
|
|
194
|
+
if (pendingToolCalls.length > 0) deferredBarrierMessages.push(chatMsg);
|
|
195
|
+
else out.push(chatMsg);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case "assistant": {
|
|
199
|
+
const aMsg = msg as OcxAssistantMessage;
|
|
200
|
+
const textParts = aMsg.content.filter(p => p.type === "text") as OcxTextContent[];
|
|
201
|
+
const thinkingParts = aMsg.content.filter(p => p.type === "thinking") as OcxThinkingContent[];
|
|
202
|
+
const toolCalls = aMsg.content.filter(p => p.type === "toolCall") as OcxToolCall[];
|
|
203
|
+
const chatMsg: Record<string, unknown> = { role: "assistant" };
|
|
204
|
+
if (textParts.length > 0) chatMsg.content = textParts.map(p => p.text).join("");
|
|
205
|
+
let reasoningContent = thinkingParts.map(p => p.thinking).join("");
|
|
206
|
+
if (
|
|
207
|
+
reasoningContent.length === 0
|
|
208
|
+
&& toolCalls.length > 0
|
|
209
|
+
&& modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
210
|
+
) {
|
|
211
|
+
const cached = toolCalls
|
|
212
|
+
.map(tc => (tc.id ? peekReasoningForCall(tc.id, replayCacheScope) : undefined))
|
|
213
|
+
.filter((text): text is string => typeof text === "string" && text.length > 0);
|
|
214
|
+
// Parallel calls share one preceding reasoning block, which is
|
|
215
|
+
// recorded under every call id — join unique texts only.
|
|
216
|
+
if (cached.length > 0) {
|
|
217
|
+
reasoningContent = [...new Set(cached)].join("\n");
|
|
218
|
+
} else if (modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)) {
|
|
219
|
+
// Fallback (extends #950, closes #1193): the replay cache is
|
|
220
|
+
// bounded (64 entries / 256 KiB / 1 h TTL) and always misses on
|
|
221
|
+
// long sessions, and some tool rounds carry no recorded reasoning
|
|
222
|
+
// at all. DeepSeek thinking mode rejects ANY tool_call assistant
|
|
223
|
+
// message missing reasoning_content with HTTP 400, so inject a
|
|
224
|
+
// minimal placeholder rather than emit a bare continuation the
|
|
225
|
+
// upstream will reject. Scoped to requiresReasoningPlaceholderModels
|
|
226
|
+
// (defaulting to the preserve list): preserve-listed providers with
|
|
227
|
+
// toggleable thinking (MiniMax low effort) opt out with `[]` so
|
|
228
|
+
// non-thinking histories are never given a fabricated placeholder.
|
|
229
|
+
reasoningContent = " ";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (reasoningContent.length > 0 && modelInList(provider.preserveReasoningContentModels, parsed.modelId)) {
|
|
233
|
+
// MiniMax's interleaved-thinking contract requires the structured
|
|
234
|
+
// reasoning_details array back on the next turn; a reasoning_content
|
|
235
|
+
// string is the native-format pass-back the docs mark unsupported.
|
|
236
|
+
if (modelInList(provider.reasoningDetailsModels, parsed.modelId)) {
|
|
237
|
+
chatMsg.reasoning_details = [reasoningDetailSegmentForWire(reasoningContent)];
|
|
238
|
+
} else {
|
|
239
|
+
chatMsg.reasoning_content = reasoningContent;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const hasReplayedReasoning = chatMsg.reasoning_content !== undefined || chatMsg.reasoning_details !== undefined;
|
|
243
|
+
if (chatMsg.content === undefined && toolCalls.length === 0 && !hasReplayedReasoning) break;
|
|
244
|
+
flushPendingToolCalls();
|
|
245
|
+
const wireToolCalls = toolCalls.map(tc => {
|
|
246
|
+
let id = tc.id;
|
|
247
|
+
if (!id) id = mintCallId();
|
|
248
|
+
else seenWireCallIds.add(id);
|
|
249
|
+
return { tc, id };
|
|
250
|
+
});
|
|
251
|
+
if (wireToolCalls.length > 0) {
|
|
252
|
+
chatMsg.tool_calls = wireToolCalls.map(({ tc, id }) => ({
|
|
253
|
+
id,
|
|
254
|
+
type: "function",
|
|
255
|
+
function: { name: namespacedToolName(tc.namespace, tc.name), arguments: JSON.stringify(tc.arguments) },
|
|
256
|
+
}));
|
|
257
|
+
if (!chatMsg.content) chatMsg.content = emptyAssistantContent(provider);
|
|
258
|
+
}
|
|
259
|
+
if (hasReplayedReasoning && chatMsg.content === undefined && chatMsg.tool_calls === undefined) {
|
|
260
|
+
chatMsg.content = emptyAssistantContent(provider);
|
|
261
|
+
}
|
|
262
|
+
out.push(chatMsg);
|
|
263
|
+
pendingToolCalls = wireToolCalls.map(({ tc, id }) => ({ id, name: namespacedToolName(tc.namespace, tc.name) }));
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case "toolResult": {
|
|
267
|
+
let toolCallId = msg.toolCallId;
|
|
268
|
+
const matchIdx = toolCallId ? pendingToolCalls.findIndex(c => c.id === toolCallId) : -1;
|
|
269
|
+
if (matchIdx >= 0 && toolCallId) {
|
|
270
|
+
out.push({
|
|
271
|
+
role: "tool",
|
|
272
|
+
tool_call_id: toolCallId,
|
|
273
|
+
content: toolResultTextForWire(msg.content, provider.annotateEmptyToolOutputs === true),
|
|
274
|
+
});
|
|
275
|
+
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
276
|
+
pendingToolCalls.splice(matchIdx, 1);
|
|
277
|
+
if (pendingToolCalls.length === 0) {
|
|
278
|
+
flushToolResultImages();
|
|
279
|
+
releaseDeferredBarriers();
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
if (!toolCallId) toolCallId = `call_orphan_${out.length}`;
|
|
283
|
+
flushPendingToolCalls();
|
|
284
|
+
const name = safeToolName(msg.toolName);
|
|
285
|
+
const cachedReasoning =
|
|
286
|
+
toolCallId && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
287
|
+
? peekReasoningForCall(toolCallId, replayCacheScope)
|
|
288
|
+
: undefined;
|
|
289
|
+
// Same fallback as the main-assistant path: never emit a bare orphan
|
|
290
|
+
// tool_call continuation on a thinking-mode provider — inject a
|
|
291
|
+
// placeholder when the replay cache missed (the bounded cache can
|
|
292
|
+
// always miss on long sessions), or DeepSeek thinking mode 400s.
|
|
293
|
+
// Gate on the preserve list too: reasoning_content is only ever
|
|
294
|
+
// serialized for preserve-listed models, so a requires-only custom
|
|
295
|
+
// entry must not fabricate it on this path (P2 on #1205).
|
|
296
|
+
// `||` (not `??`): the cache never stores empty strings, but treat a
|
|
297
|
+
// falsy hit as a miss so the placeholder still fires.
|
|
298
|
+
const orphanReasoning =
|
|
299
|
+
cachedReasoning
|
|
300
|
+
|| (modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
301
|
+
&& modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)
|
|
302
|
+
? " "
|
|
303
|
+
: undefined);
|
|
304
|
+
const orphanReasoningFields: Record<string, unknown> = !orphanReasoning
|
|
305
|
+
? {}
|
|
306
|
+
: modelInList(provider.reasoningDetailsModels, parsed.modelId)
|
|
307
|
+
? { reasoning_details: [reasoningDetailSegmentForWire(orphanReasoning)] }
|
|
308
|
+
: { reasoning_content: orphanReasoning };
|
|
309
|
+
out.push({
|
|
310
|
+
role: "assistant",
|
|
311
|
+
content: emptyAssistantContent(provider),
|
|
312
|
+
...orphanReasoningFields,
|
|
313
|
+
tool_calls: [{
|
|
314
|
+
id: toolCallId,
|
|
315
|
+
type: "function",
|
|
316
|
+
function: { name, arguments: "{}" },
|
|
317
|
+
}],
|
|
318
|
+
});
|
|
319
|
+
seenWireCallIds.add(toolCallId);
|
|
320
|
+
out.push({
|
|
321
|
+
role: "tool",
|
|
322
|
+
tool_call_id: toolCallId,
|
|
323
|
+
content: toolResultTextForWire(msg.content, provider.annotateEmptyToolOutputs === true),
|
|
324
|
+
});
|
|
325
|
+
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
326
|
+
flushToolResultImages();
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
flushPendingToolCalls();
|
|
334
|
+
releaseDeferredBarriers();
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function safeToolName(name: string | undefined): string {
|
|
339
|
+
const raw = name && name.trim().length > 0 ? name : "tool_result";
|
|
340
|
+
const sanitized = raw.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
341
|
+
return sanitized;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function emptyAssistantContent(provider: OcxProviderConfig): string | { type: "text"; text: string }[] {
|
|
345
|
+
return isVolcengineArkPaygChatTarget(provider) ? [{ type: "text", text: "" }] : "";
|
|
346
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { openAIChatTransport, stripBracketedModelSuffix } from "./wire";
|
|
2
|
+
import type { AdapterRequest } from "../base";
|
|
3
|
+
import { frameAgentRouterMessages } from "../agentrouter";
|
|
4
|
+
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../../providers/openrouter-routing";
|
|
5
|
+
import { resolveVercelGatewayRouting, vercelGatewayProviderPayload } from "../../providers/vercel-gateway-routing";
|
|
6
|
+
import { fastPolicyForModel } from "../../providers/service-tier";
|
|
7
|
+
import { canonicalFastTierMarker, decideTier, type ResolvedFastPolicy } from "../../providers/fastwire";
|
|
8
|
+
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
9
|
+
import { isDebugEnabled } from "../../lib/debug-settings";
|
|
10
|
+
import { modelRecordValue } from "../../reasoning-effort";
|
|
11
|
+
import { modelInList, type OcxProviderConfig } from "../../types";
|
|
12
|
+
|
|
13
|
+
const CHAT_PASSTHROUGH_FIELDS = [
|
|
14
|
+
"audio",
|
|
15
|
+
"frequency_penalty",
|
|
16
|
+
"logit_bias",
|
|
17
|
+
"logprobs",
|
|
18
|
+
"max_completion_tokens",
|
|
19
|
+
"max_tokens",
|
|
20
|
+
"metadata",
|
|
21
|
+
"modalities",
|
|
22
|
+
"n",
|
|
23
|
+
"prediction",
|
|
24
|
+
"presence_penalty",
|
|
25
|
+
"reasoning_effort",
|
|
26
|
+
"response_format",
|
|
27
|
+
"seed",
|
|
28
|
+
"stop",
|
|
29
|
+
"store",
|
|
30
|
+
"temperature",
|
|
31
|
+
"tool_choice",
|
|
32
|
+
"tools",
|
|
33
|
+
"top_logprobs",
|
|
34
|
+
"top_p",
|
|
35
|
+
"user",
|
|
36
|
+
"web_search_options",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a provider request from an inbound Chat Completions body without translating it
|
|
41
|
+
* through the Responses contract. This is deliberately a whitelist: Chat-only caller
|
|
42
|
+
* fields retain their exact wire representation, while provider capability gates remain
|
|
43
|
+
* centralized beside the ordinary openai-chat adapter.
|
|
44
|
+
*/
|
|
45
|
+
export function buildOpenAIChatPassthroughRequest(
|
|
46
|
+
provider: OcxProviderConfig,
|
|
47
|
+
rawBody: Record<string, unknown>,
|
|
48
|
+
modelId: string,
|
|
49
|
+
stream: boolean,
|
|
50
|
+
fastPolicy: ResolvedFastPolicy = fastPolicyForModel(provider, modelId, undefined, "chat"),
|
|
51
|
+
fastMode?: boolean,
|
|
52
|
+
): AdapterRequest {
|
|
53
|
+
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
54
|
+
|
|
55
|
+
const body: Record<string, unknown> = {
|
|
56
|
+
model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(modelId) : modelId,
|
|
57
|
+
messages: frameAgentRouterMessages(provider.baseUrl, rawBody.messages),
|
|
58
|
+
stream,
|
|
59
|
+
};
|
|
60
|
+
for (const field of CHAT_PASSTHROUGH_FIELDS) {
|
|
61
|
+
if (rawBody[field] !== undefined) body[field] = rawBody[field];
|
|
62
|
+
}
|
|
63
|
+
const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
|
|
64
|
+
if (modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0) {
|
|
65
|
+
delete body.reasoning_effort;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const openRouterRouting = resolveOpenRouterRouting(provider, modelId);
|
|
69
|
+
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
|
|
70
|
+
const vercelRouting = resolveVercelGatewayRouting(provider, modelId);
|
|
71
|
+
if (vercelRouting) body.provider = vercelGatewayProviderPayload(vercelRouting);
|
|
72
|
+
|
|
73
|
+
if (modelInList(provider.noTemperatureModels, modelId)) delete body.temperature;
|
|
74
|
+
if (modelInList(provider.noTopPModels, modelId)) delete body.top_p;
|
|
75
|
+
if (modelInList(provider.noPenaltyModels, modelId)) {
|
|
76
|
+
delete body.presence_penalty;
|
|
77
|
+
delete body.frequency_penalty;
|
|
78
|
+
}
|
|
79
|
+
// Exact match, unlike the gates above: `noStructuredOutputModels` is documented as
|
|
80
|
+
// "only an exact requested-model match omits the field" (#1424), and the Responses
|
|
81
|
+
// ingress enforces exactly that. A prefix match here would strip response_format from
|
|
82
|
+
// `<listed>:<tag>` siblings the operator never opted out, silently returning prose.
|
|
83
|
+
if (provider.noStructuredOutputModels?.includes(modelId)) delete body.response_format;
|
|
84
|
+
// Narrower neighbour: the model takes `json_object` but rejects `json_schema`. Downgrade
|
|
85
|
+
// rather than drop, so a caller that asked for JSON still gets JSON. The type check also
|
|
86
|
+
// makes the kill switch above win without an else — after its `delete` there is no type
|
|
87
|
+
// left to match.
|
|
88
|
+
const passthroughFormat = body.response_format;
|
|
89
|
+
if (provider.noJsonSchemaModels?.includes(modelId)
|
|
90
|
+
&& typeof passthroughFormat === "object" && passthroughFormat !== null
|
|
91
|
+
&& (passthroughFormat as { type?: unknown }).type === "json_schema") {
|
|
92
|
+
body.response_format = { type: "json_object" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Run the same complete Fast policy as the translated Chat path, including explicit
|
|
96
|
+
// fastMode and foreign-tier handling. On inherited canonical Fast, the passthrough still
|
|
97
|
+
// retains the caller's exact spelling; forced Fast uses the policy-owned wire value.
|
|
98
|
+
const callerTier = typeof rawBody.service_tier === "string" ? rawBody.service_tier : undefined;
|
|
99
|
+
const tierDecision = decideTier(fastPolicy, fastMode, callerTier);
|
|
100
|
+
if (tierDecision.kind === "set") {
|
|
101
|
+
body.service_tier = fastMode === undefined && canonicalFastTierMarker(callerTier) !== undefined
|
|
102
|
+
? callerTier
|
|
103
|
+
: tierDecision.value;
|
|
104
|
+
} else if (tierDecision.kind === "forward-caller" && rawBody.service_tier !== undefined) {
|
|
105
|
+
body.service_tier = rawBody.service_tier;
|
|
106
|
+
}
|
|
107
|
+
if (provider.promptCacheKey && rawBody.prompt_cache_key !== undefined) {
|
|
108
|
+
body.prompt_cache_key = rawBody.prompt_cache_key;
|
|
109
|
+
}
|
|
110
|
+
if (Array.isArray(rawBody.tools) && rawBody.tools.length > 0) {
|
|
111
|
+
if (provider.parallelToolCalls === true) {
|
|
112
|
+
body.parallel_tool_calls = rawBody.parallel_tool_calls !== false;
|
|
113
|
+
} else if (provider.parallelToolCalls === false
|
|
114
|
+
&& (provider.baseUrl === "https://integrate.api.nvidia.com/v1" || provider.pinParallelToolCallsFalse === true)) {
|
|
115
|
+
body.parallel_tool_calls = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (stream) {
|
|
119
|
+
const callerOptions = rawBody.stream_options !== null
|
|
120
|
+
&& typeof rawBody.stream_options === "object"
|
|
121
|
+
&& !Array.isArray(rawBody.stream_options)
|
|
122
|
+
? rawBody.stream_options as Record<string, unknown>
|
|
123
|
+
: {};
|
|
124
|
+
body.stream_options = { ...callerOptions, include_usage: true };
|
|
125
|
+
} else if (rawBody.stream_options !== undefined) {
|
|
126
|
+
body.stream_options = rawBody.stream_options;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const bodyJson = JSON.stringify(body);
|
|
130
|
+
|
|
131
|
+
if (isDebugEnabled()) {
|
|
132
|
+
let host = "upstream";
|
|
133
|
+
try { host = new URL(url).host; } catch { /* keep fallback */ }
|
|
134
|
+
debugProviderDiagnostic("openai-chat", "passthrough-request", {
|
|
135
|
+
host,
|
|
136
|
+
model: body.model,
|
|
137
|
+
stream,
|
|
138
|
+
messageCount: Array.isArray(body.messages) ? body.messages.length : 0,
|
|
139
|
+
toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
|
|
140
|
+
hasCredential,
|
|
141
|
+
bodyBytes: Buffer.byteLength(bodyJson, "utf8"),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { url, method: "POST", headers, body: bodyJson };
|
|
146
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { diagnoseInvalidToolCalls, isRecord, type InvalidToolCallDiagnostic } from "./tool-call-validation";
|
|
2
|
+
import type { AdapterEvent, OcxUsage } from "../../types";
|
|
3
|
+
|
|
4
|
+
export function stopReasonFor(finishReason: unknown): "max_tokens" | "content_filter" | undefined {
|
|
5
|
+
return finishReason === "length"
|
|
6
|
+
? "max_tokens"
|
|
7
|
+
: finishReason === "content_filter"
|
|
8
|
+
? "content_filter"
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function reasoningTextFrom(record: Record<string, unknown>): string | undefined {
|
|
13
|
+
return typeof record.reasoning_content === "string" && record.reasoning_content.length > 0
|
|
14
|
+
? record.reasoning_content
|
|
15
|
+
: typeof record.reasoning === "string" && record.reasoning.length > 0
|
|
16
|
+
? record.reasoning
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ReasoningDetailSegment {
|
|
21
|
+
key: string;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Structured `reasoning_details` array (MiniMax M-series with `reasoning_split`).
|
|
27
|
+
* Each segment's key scopes cumulative-snapshot tracking: upstream repeats the
|
|
28
|
+
* full text-so-far under a stable `id`/`index` instead of sending increments.
|
|
29
|
+
*/
|
|
30
|
+
export function reasoningDetailSegmentsFrom(record: Record<string, unknown>): ReasoningDetailSegment[] {
|
|
31
|
+
const raw = record.reasoning_details;
|
|
32
|
+
if (!Array.isArray(raw)) return [];
|
|
33
|
+
const segments: ReasoningDetailSegment[] = [];
|
|
34
|
+
for (let i = 0; i < raw.length; i++) {
|
|
35
|
+
const item: unknown = raw[i];
|
|
36
|
+
if (!isRecord(item)) continue;
|
|
37
|
+
if (typeof item.text !== "string" || item.text.length === 0) continue;
|
|
38
|
+
const key = typeof item.id === "string" && item.id.length > 0
|
|
39
|
+
? `id:${item.id}`
|
|
40
|
+
: typeof item.index === "number"
|
|
41
|
+
? `i:${item.index}`
|
|
42
|
+
: `n:${i}`;
|
|
43
|
+
segments.push({ key, text: item.text });
|
|
44
|
+
}
|
|
45
|
+
return segments;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Single-segment `reasoning_details` entry for replaying preserved reasoning (MiniMax wire shape). */
|
|
49
|
+
export function reasoningDetailSegmentForWire(text: string): Record<string, unknown> {
|
|
50
|
+
return { type: "reasoning.text", id: "reasoning-text-1", format: "MiniMax-response-v1", index: 0, text };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function invalidChoicesEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> {
|
|
54
|
+
return {
|
|
55
|
+
type: "error",
|
|
56
|
+
message: "upstream response contained invalid choices",
|
|
57
|
+
...(usage !== undefined ? { usage } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function invalidToolCallsEvent(
|
|
62
|
+
rawToolCalls: unknown,
|
|
63
|
+
mode: "stream" | "response",
|
|
64
|
+
usage?: OcxUsage,
|
|
65
|
+
diagnosticOverride?: InvalidToolCallDiagnostic,
|
|
66
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
67
|
+
// The streamed accumulator knows things a rescan cannot: which field on which pending call
|
|
68
|
+
// was actually rejected. Without the override, a stream carrying accepted padding on call 0
|
|
69
|
+
// and a real defect on call 1 blames call 0, because the stateless scan stops at the first
|
|
70
|
+
// structurally odd value it sees.
|
|
71
|
+
const diagnostic = diagnosticOverride ?? diagnoseInvalidToolCalls(rawToolCalls, mode);
|
|
72
|
+
const detail = diagnostic
|
|
73
|
+
? ` (${diagnostic.reason}${diagnostic.callIndex !== undefined ? `; callIndex=${diagnostic.callIndex}` : ""}; valueType=${diagnostic.valueType})`
|
|
74
|
+
: "";
|
|
75
|
+
return {
|
|
76
|
+
type: "error",
|
|
77
|
+
status: 502,
|
|
78
|
+
errorType: "upstream_error",
|
|
79
|
+
message: `upstream response contained invalid tool calls${detail}`,
|
|
80
|
+
...(usage !== undefined ? { usage } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A streamed tool call is only dispatchable once the upstream has named the function.
|
|
86
|
+
*
|
|
87
|
+
* The OpenAI streaming convention puts `function.name` in the first chunk for a tool-call
|
|
88
|
+
* index and leaves later chunks carrying only `arguments` deltas, so a stream that never
|
|
89
|
+
* sends a name is non-conforming for every provider rather than quirky for one. The
|
|
90
|
+
* reference implementations accumulate such a call with an empty name and let the caller
|
|
91
|
+
* fail; we sit at the boundary where it would become a Codex tool-call contract event, so
|
|
92
|
+
* the equivalent is to refuse to emit it.
|
|
93
|
+
*
|
|
94
|
+
* Failing closed rather than dropping is deliberate, and matches #1325: a claimed tool call
|
|
95
|
+
* that silently disappears can leave the matching result orphaned on the next turn. Naming
|
|
96
|
+
* it ourselves is worse still — the id is synthesizable because it is an opaque correlation
|
|
97
|
+
* handle, but a function name is a guess at intent.
|
|
98
|
+
*/
|
|
99
|
+
export function unnamedToolCallEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> {
|
|
100
|
+
return {
|
|
101
|
+
type: "error",
|
|
102
|
+
message: "upstream streamed a tool call without a function name — cannot dispatch",
|
|
103
|
+
...(usage !== undefined ? { usage } : {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function usageFromOpenAIChat(usage: Record<string, unknown> | undefined): OcxUsage | undefined {
|
|
108
|
+
if (!usage) return undefined;
|
|
109
|
+
const promptDetails = usage.prompt_tokens_details as Record<string, number> | undefined;
|
|
110
|
+
const completionDetails = usage.completion_tokens_details as Record<string, number> | undefined;
|
|
111
|
+
return {
|
|
112
|
+
inputTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
|
|
113
|
+
outputTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0,
|
|
114
|
+
...(promptDetails?.cached_tokens !== undefined ? { cachedInputTokens: promptDetails.cached_tokens } : {}),
|
|
115
|
+
...(completionDetails?.reasoning_tokens !== undefined ? { reasoningOutputTokens: completionDetails.reasoning_tokens } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|