@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
|
@@ -7,7 +7,8 @@ import { isDebugEnabled } from "../lib/debug-settings";
|
|
|
7
7
|
import { isCyberPolicyCode } from "../lib/errors";
|
|
8
8
|
import { redactSecretString } from "../lib/redact";
|
|
9
9
|
import { contentPartsToText } from "./image";
|
|
10
|
-
import {
|
|
10
|
+
import { identifyRoutedModel } from "./identity";
|
|
11
|
+
import { peekReasoningForCall } from "../responses/reasoning-replay-cache";
|
|
11
12
|
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
|
|
12
13
|
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
|
|
13
14
|
import {
|
|
@@ -70,6 +71,103 @@ function extractErrorDetail(parsed: unknown): string | undefined {
|
|
|
70
71
|
return undefined;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
// ClinePass live responses observed 2026-08-02 wrap non-stream Chat Completions in
|
|
75
|
+
// `{ success, error, data }`; its public Chat Completions docs do not currently describe that
|
|
76
|
+
// envelope. Keep ordinary OpenAI-shaped responses on the direct path.
|
|
77
|
+
function unwrapChatCompletionPayload(json: Record<string, unknown>): Record<string, unknown> {
|
|
78
|
+
if ((json.error !== undefined && json.error !== null) || Array.isArray(json.choices)) return json;
|
|
79
|
+
const data = json.data;
|
|
80
|
+
return data !== null && typeof data === "object" && !Array.isArray(data)
|
|
81
|
+
? data as Record<string, unknown>
|
|
82
|
+
: json;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface OpenAIChatError {
|
|
86
|
+
message?: unknown;
|
|
87
|
+
code?: unknown;
|
|
88
|
+
type?: unknown;
|
|
89
|
+
status?: unknown;
|
|
90
|
+
metadata?: unknown;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function safeUpstreamRequestId(metadata: unknown): string | undefined {
|
|
94
|
+
if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) return undefined;
|
|
95
|
+
const record = metadata as Record<string, unknown>;
|
|
96
|
+
const value = record.request_id ?? record.requestId;
|
|
97
|
+
if (typeof value !== "string") return undefined;
|
|
98
|
+
const requestId = value.trim();
|
|
99
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId)
|
|
100
|
+
&& redactSecretString(requestId) === requestId
|
|
101
|
+
? requestId
|
|
102
|
+
: undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function upstreamErrorEvent(
|
|
106
|
+
error: unknown,
|
|
107
|
+
usage?: OcxUsage,
|
|
108
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
109
|
+
const details = error !== null && typeof error === "object" && !Array.isArray(error)
|
|
110
|
+
? error as OpenAIChatError
|
|
111
|
+
: undefined;
|
|
112
|
+
const rawMessage = typeof error === "string"
|
|
113
|
+
? error.trim() || "upstream error"
|
|
114
|
+
: typeof details?.message === "string" ? details.message : "upstream error";
|
|
115
|
+
const safeMessage = redactSecretString(rawMessage);
|
|
116
|
+
const requestId = safeUpstreamRequestId(details?.metadata);
|
|
117
|
+
const message = requestId !== undefined && !safeMessage.includes(requestId)
|
|
118
|
+
? `${safeMessage} (request ID: ${requestId})`
|
|
119
|
+
: safeMessage;
|
|
120
|
+
const code = typeof details?.code === "string"
|
|
121
|
+
? details.code
|
|
122
|
+
: typeof details?.code === "number" && Number.isFinite(details.code) && Number.isInteger(details.code)
|
|
123
|
+
? String(details.code)
|
|
124
|
+
: undefined;
|
|
125
|
+
const errorType = typeof details?.type === "string" ? details.type : undefined;
|
|
126
|
+
const codeStatus = typeof details?.code === "number"
|
|
127
|
+
&& Number.isInteger(details.code)
|
|
128
|
+
&& details.code >= 100
|
|
129
|
+
&& details.code <= 599
|
|
130
|
+
? details.code
|
|
131
|
+
: undefined;
|
|
132
|
+
const status = isCyberPolicyCode(code)
|
|
133
|
+
? 400
|
|
134
|
+
: typeof details?.status === "number" && Number.isInteger(details.status)
|
|
135
|
+
? details.status
|
|
136
|
+
: codeStatus;
|
|
137
|
+
return {
|
|
138
|
+
type: "error",
|
|
139
|
+
message,
|
|
140
|
+
...(usage !== undefined ? { usage } : {}),
|
|
141
|
+
...(code !== undefined ? { code } : {}),
|
|
142
|
+
...(errorType !== undefined ? { errorType } : {}),
|
|
143
|
+
...(status !== undefined ? { status } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function stopReasonFor(finishReason: unknown): "max_tokens" | "content_filter" | undefined {
|
|
148
|
+
return finishReason === "length"
|
|
149
|
+
? "max_tokens"
|
|
150
|
+
: finishReason === "content_filter"
|
|
151
|
+
? "content_filter"
|
|
152
|
+
: undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function reasoningTextFrom(record: Record<string, unknown>): string | undefined {
|
|
156
|
+
return typeof record.reasoning_content === "string" && record.reasoning_content.length > 0
|
|
157
|
+
? record.reasoning_content
|
|
158
|
+
: typeof record.reasoning === "string" && record.reasoning.length > 0
|
|
159
|
+
? record.reasoning
|
|
160
|
+
: undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function invalidChoicesEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> {
|
|
164
|
+
return {
|
|
165
|
+
type: "error",
|
|
166
|
+
message: "upstream response contained invalid choices",
|
|
167
|
+
...(usage !== undefined ? { usage } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
73
171
|
function developerSystemText(message: OcxMessage): string | undefined {
|
|
74
172
|
if (message.role !== "developer") return undefined;
|
|
75
173
|
if (typeof message.content === "string") return message.content;
|
|
@@ -77,9 +175,30 @@ function developerSystemText(message: OcxMessage): string | undefined {
|
|
|
77
175
|
return message.content.map(part => (part as OcxTextContent).text).join("");
|
|
78
176
|
}
|
|
79
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Chat-completions image_url parts for images carried inside a tool result (issue #888). role:"tool"
|
|
180
|
+
* content is text-only on every chat provider, so these ride in a follow-up user message instead of
|
|
181
|
+
* being flattened to the "[image]" marker the model can't actually see. Data URLs and remote https
|
|
182
|
+
* URLs are both valid in image_url.url, unlike Gemini inline_data which needs base64.
|
|
183
|
+
*/
|
|
184
|
+
function toolResultImageChatParts(content: string | OcxContentPart[]): unknown[] {
|
|
185
|
+
if (typeof content === "string") return [];
|
|
186
|
+
const parts: unknown[] = [];
|
|
187
|
+
for (const p of content) {
|
|
188
|
+
// Skip parts without a usable URL (the tool-output parser accepts the empty file_id shape):
|
|
189
|
+
// a {"url":""} part would fail the whole request where the "[image]" marker degrades safely.
|
|
190
|
+
if (p.type !== "image" || !p.imageUrl) continue;
|
|
191
|
+
parts.push({ type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } });
|
|
192
|
+
}
|
|
193
|
+
return parts;
|
|
194
|
+
}
|
|
195
|
+
|
|
80
196
|
function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
|
|
81
197
|
const out: unknown[] = [];
|
|
82
198
|
const { context, options } = parsed;
|
|
199
|
+
// Mirror the bridge's replay-cache scope (issue #950): provider call ids are
|
|
200
|
+
// not globally unique, so reasoning must not cross conversation boundaries.
|
|
201
|
+
const replayCacheScope = parsed._clientThreadId ?? "global";
|
|
83
202
|
|
|
84
203
|
// 260718 dangling tool_calls hardening (devlog/_plan/260718_dangling_toolcall_hardening):
|
|
85
204
|
// strict chat providers (Kimi/Moonshot) 400 when an assistant tool_call is not answered
|
|
@@ -91,6 +210,7 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
91
210
|
interface PendingToolCall { id: string; name: string }
|
|
92
211
|
let pendingToolCalls: PendingToolCall[] = [];
|
|
93
212
|
let deferredBarrierMessages: unknown[] = [];
|
|
213
|
+
let pendingToolResultImageParts: unknown[] = [];
|
|
94
214
|
let mintedIdSeq = 0;
|
|
95
215
|
const seenWireCallIds = new Set<string>();
|
|
96
216
|
|
|
@@ -109,6 +229,22 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
109
229
|
deferredBarrierMessages = [];
|
|
110
230
|
};
|
|
111
231
|
|
|
232
|
+
// Tool-result images collected during the open round land in ONE user vision message once the
|
|
233
|
+
// round closes — never inside it, where strict providers (Kimi/Moonshot) 400 on interleaved
|
|
234
|
+
// user messages. Released before deferred barriers so the images stay adjacent to the results
|
|
235
|
+
// they came from (mirrors google.ts sibling inline_data parts and the Kiro carrier images).
|
|
236
|
+
const flushToolResultImages = (): void => {
|
|
237
|
+
if (pendingToolResultImageParts.length === 0) return;
|
|
238
|
+
out.push({
|
|
239
|
+
role: "user",
|
|
240
|
+
content: [
|
|
241
|
+
{ type: "text", text: "[ocx] image output from the preceding tool result(s):" },
|
|
242
|
+
...pendingToolResultImageParts,
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
pendingToolResultImageParts = [];
|
|
246
|
+
};
|
|
247
|
+
|
|
112
248
|
// Close an unresolved tool round with explicit unavailable-result messages. The wording
|
|
113
249
|
// must not claim interruption, success, failure, or user intent: execution status is
|
|
114
250
|
// UNKNOWN, and for user-input tools this must not read as an answer.
|
|
@@ -122,6 +258,7 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
122
258
|
});
|
|
123
259
|
}
|
|
124
260
|
pendingToolCalls = [];
|
|
261
|
+
flushToolResultImages();
|
|
125
262
|
releaseDeferredBarriers();
|
|
126
263
|
};
|
|
127
264
|
|
|
@@ -147,7 +284,10 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
147
284
|
// base_instructions is ignored at request time). Neutralize that one identity line
|
|
148
285
|
// so routed, non-OpenAI models don't misreport themselves as GPT-5 / OpenAI — without
|
|
149
286
|
// leaking the proxy identity into the payload.
|
|
150
|
-
const
|
|
287
|
+
const wireModelId = provider.modelSuffixBracketStrip
|
|
288
|
+
? stripBracketedModelSuffix(parsed.modelId)
|
|
289
|
+
: parsed.modelId;
|
|
290
|
+
const sys = identifyRoutedModel(systemParts.join("\n\n"), wireModelId);
|
|
151
291
|
out.push({ role: "system", content: sys });
|
|
152
292
|
}
|
|
153
293
|
|
|
@@ -188,7 +328,26 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
188
328
|
if (textParts.length > 0) {
|
|
189
329
|
chatMsg.content = textParts.map(p => p.text).join("");
|
|
190
330
|
}
|
|
191
|
-
|
|
331
|
+
let reasoningContent = thinkingParts.map(p => p.thinking).join("");
|
|
332
|
+
// History transformations (compaction, lost assistant turn, resumed
|
|
333
|
+
// threads) can strip the reasoning item while the tool round survives.
|
|
334
|
+
// Re-attach the reasoning the bridge recorded for these call ids so
|
|
335
|
+
// preserveReasoningContentModels providers (DeepSeek thinking mode)
|
|
336
|
+
// never receive a bare tool-call continuation (issue #950).
|
|
337
|
+
if (
|
|
338
|
+
reasoningContent.length === 0
|
|
339
|
+
&& toolCalls.length > 0
|
|
340
|
+
&& modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
341
|
+
) {
|
|
342
|
+
const cached = toolCalls
|
|
343
|
+
.map(tc => (tc.id ? peekReasoningForCall(tc.id, replayCacheScope) : undefined))
|
|
344
|
+
.filter((text): text is string => typeof text === "string" && text.length > 0);
|
|
345
|
+
// Parallel calls share one preceding reasoning block, which is
|
|
346
|
+
// recorded under every call id — join unique texts only.
|
|
347
|
+
if (cached.length > 0) {
|
|
348
|
+
reasoningContent = [...new Set(cached)].join("\n");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
192
351
|
if (reasoningContent.length > 0 && modelInList(provider.preserveReasoningContentModels, parsed.modelId)) {
|
|
193
352
|
chatMsg.reasoning_content = reasoningContent;
|
|
194
353
|
}
|
|
@@ -232,8 +391,12 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
232
391
|
tool_call_id: toolCallId,
|
|
233
392
|
content: contentPartsToText(msg.content),
|
|
234
393
|
});
|
|
394
|
+
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
235
395
|
pendingToolCalls.splice(matchIdx, 1);
|
|
236
|
-
if (pendingToolCalls.length === 0)
|
|
396
|
+
if (pendingToolCalls.length === 0) {
|
|
397
|
+
flushToolResultImages();
|
|
398
|
+
releaseDeferredBarriers();
|
|
399
|
+
}
|
|
237
400
|
} else {
|
|
238
401
|
if (!toolCallId) toolCallId = `call_orphan_${out.length}`;
|
|
239
402
|
// No matching call in the open round. Close any unresolved round first so the
|
|
@@ -242,9 +405,17 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
242
405
|
// role:"tool" message unless an assistant tool_call with the same id immediately precedes it.
|
|
243
406
|
flushPendingToolCalls();
|
|
244
407
|
const name = safeToolName(msg.toolName);
|
|
408
|
+
// The orphan repair synthesizes an assistant tool call for a result
|
|
409
|
+
// whose assistant turn was lost; carry the recorded reasoning so the
|
|
410
|
+
// replayed round stays valid for thinking-mode providers (#950).
|
|
411
|
+
const cachedReasoning =
|
|
412
|
+
toolCallId && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
413
|
+
? peekReasoningForCall(toolCallId, replayCacheScope)
|
|
414
|
+
: undefined;
|
|
245
415
|
out.push({
|
|
246
416
|
role: "assistant",
|
|
247
417
|
content: emptyAssistantContent(provider),
|
|
418
|
+
...(cachedReasoning ? { reasoning_content: cachedReasoning } : {}),
|
|
248
419
|
tool_calls: [{
|
|
249
420
|
id: toolCallId,
|
|
250
421
|
type: "function",
|
|
@@ -257,6 +428,8 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
257
428
|
tool_call_id: toolCallId,
|
|
258
429
|
content: contentPartsToText(msg.content),
|
|
259
430
|
});
|
|
431
|
+
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
432
|
+
flushToolResultImages();
|
|
260
433
|
}
|
|
261
434
|
break;
|
|
262
435
|
}
|
|
@@ -370,14 +543,6 @@ function isXaiSchemaTarget(provider: OcxProviderConfig): boolean {
|
|
|
370
543
|
}
|
|
371
544
|
}
|
|
372
545
|
|
|
373
|
-
function isKimiSchemaTarget(provider: OcxProviderConfig): boolean {
|
|
374
|
-
try {
|
|
375
|
-
return new URL(provider.baseUrl).hostname === "api.kimi.com";
|
|
376
|
-
} catch {
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
546
|
// Volcengine Ark regional endpoints. Ark validates an assistant message's text field as a
|
|
382
547
|
// REQUIRED parameter and treats "" as absent, so a tool-call-only assistant in history 400s with
|
|
383
548
|
// `MissingParameter: input.content.text` (#796). Every other OpenAI-compatible provider accepts
|
|
@@ -416,11 +581,16 @@ function emptyAssistantContent(provider: OcxProviderConfig): string | { type: "t
|
|
|
416
581
|
}
|
|
417
582
|
|
|
418
583
|
/**
|
|
419
|
-
* Kimi
|
|
420
|
-
*
|
|
421
|
-
* Add type: "object" at the root while preserving
|
|
584
|
+
* Providers like Kimi and DeepSeek reject function parameter schemas whose root
|
|
585
|
+
* `type` is missing or `null` — JSON Schema requires `"object"` at the root of
|
|
586
|
+
* function parameters. Add `type: "object"` at the root while preserving
|
|
587
|
+
* `oneOf`, `$defs`, and every other schema key.
|
|
588
|
+
*
|
|
589
|
+
* This mirrors `normalizeFunctionToolSchema` in openai-responses.ts, which
|
|
590
|
+
* applies the same root-only normalization unconditionally on the responses
|
|
591
|
+
* path. Nested schema content is intentionally left untouched.
|
|
422
592
|
*/
|
|
423
|
-
function
|
|
593
|
+
function ensureRootObjectType(parameters: unknown): Record<string, unknown> {
|
|
424
594
|
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
|
425
595
|
return { type: "object", properties: {} };
|
|
426
596
|
}
|
|
@@ -471,13 +641,11 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig
|
|
|
471
641
|
: parsed.context.tools;
|
|
472
642
|
if (tools.length === 0) return undefined;
|
|
473
643
|
const xaiTarget = isXaiSchemaTarget(provider);
|
|
474
|
-
const kimiTarget = isKimiSchemaTarget(provider);
|
|
475
644
|
const formatted = tools.flatMap(t => {
|
|
476
645
|
const parameters = xaiTarget
|
|
477
646
|
? normalizeXaiToolParameters(t.parameters)
|
|
478
|
-
:
|
|
479
|
-
|
|
480
|
-
: t.parameters;
|
|
647
|
+
: ensureRootObjectType(t.parameters);
|
|
648
|
+
|
|
481
649
|
if (parameters === undefined) return [];
|
|
482
650
|
return [{
|
|
483
651
|
type: "function",
|
|
@@ -588,10 +756,27 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
588
756
|
body.top_p = parsed.options.topP;
|
|
589
757
|
}
|
|
590
758
|
if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
|
|
759
|
+
const reasoningDisabled = modelInList(provider.noReasoningModels, parsed.modelId);
|
|
591
760
|
const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
592
761
|
let reasoningLog: AdapterRequest["reasoningLog"];
|
|
593
|
-
|
|
594
|
-
|
|
762
|
+
// ClinePass live requests observed 2026-08-02 require this gateway-specific object; the
|
|
763
|
+
// public API docs do not currently specify its request shape.
|
|
764
|
+
if (!reasoningDisabled && provider.reasoningWireFormat === "gateway-object" && parsed.options.reasoning === "none") {
|
|
765
|
+
body.reasoning = { enabled: false };
|
|
766
|
+
reasoningLog = {
|
|
767
|
+
effectiveEffort: "none",
|
|
768
|
+
wireField: "reasoning.enabled",
|
|
769
|
+
wireValue: false,
|
|
770
|
+
};
|
|
771
|
+
} else if (reasoningEffort !== undefined) {
|
|
772
|
+
if (provider.reasoningWireFormat === "gateway-object") {
|
|
773
|
+
body.reasoning = { enabled: true, effort: reasoningEffort };
|
|
774
|
+
reasoningLog = {
|
|
775
|
+
effectiveEffort: reasoningEffort,
|
|
776
|
+
wireField: "reasoning.effort",
|
|
777
|
+
wireValue: reasoningEffort,
|
|
778
|
+
};
|
|
779
|
+
} else if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
|
|
595
780
|
const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
|
|
596
781
|
if (budget !== undefined) {
|
|
597
782
|
body.thinking_budget = budget;
|
|
@@ -704,17 +889,28 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
704
889
|
interface PendingToolCall { key: string; id: string; name: string; args: string; argsBytes: number }
|
|
705
890
|
const pendingToolCalls: PendingToolCall[] = [];
|
|
706
891
|
let toolCallSeq = 0;
|
|
892
|
+
const closeToolCalls = (): PendingToolCall[] => {
|
|
893
|
+
const calls = [...pendingToolCalls];
|
|
894
|
+
for (const call of calls) budget.closeCall(call.key);
|
|
895
|
+
pendingToolCalls.length = 0;
|
|
896
|
+
return calls;
|
|
897
|
+
};
|
|
707
898
|
const flushToolCalls = function* (): Generator<AdapterEvent> {
|
|
708
899
|
// Do not treat flushed tool calls as user-facing output for the finish-less EOF
|
|
709
900
|
// fallback — incomplete tool args must stay on the truncation path.
|
|
710
|
-
for (const call of
|
|
901
|
+
for (const call of closeToolCalls()) {
|
|
711
902
|
if (!call.id) call.id = `call_${++toolCallSeq}`;
|
|
712
903
|
yield { type: "tool_call_start", id: call.id, name: call.name };
|
|
713
904
|
if (call.args.length > 0) yield { type: "tool_call_delta", arguments: call.args };
|
|
714
905
|
yield { type: "tool_call_end" };
|
|
715
|
-
budget.closeCall(call.key);
|
|
716
906
|
}
|
|
717
|
-
|
|
907
|
+
};
|
|
908
|
+
const terminateWithError = function* (
|
|
909
|
+
event: Extract<AdapterEvent, { type: "error" }>,
|
|
910
|
+
): Generator<AdapterEvent, "terminate"> {
|
|
911
|
+
closeToolCalls();
|
|
912
|
+
yield event;
|
|
913
|
+
return "terminate";
|
|
718
914
|
};
|
|
719
915
|
let pendingUsage: OcxUsage | undefined;
|
|
720
916
|
// Track terminal signals so a socket EOF without any terminator can fail closed instead of
|
|
@@ -735,11 +931,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
735
931
|
const payload = line.slice(6).trim();
|
|
736
932
|
if (payload === "[DONE]") {
|
|
737
933
|
yield* flushToolCalls();
|
|
738
|
-
const stopReason = finishReason
|
|
739
|
-
? "max_tokens"
|
|
740
|
-
: finishReason === "content_filter"
|
|
741
|
-
? "content_filter"
|
|
742
|
-
: undefined;
|
|
934
|
+
const stopReason = stopReasonFor(finishReason);
|
|
743
935
|
yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
|
|
744
936
|
return "terminate";
|
|
745
937
|
}
|
|
@@ -755,23 +947,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
755
947
|
// A 200/OK chat-completions stream may carry an inline provider error envelope
|
|
756
948
|
// instead of a clean [DONE]. Surface it as a terminal error so the bridge emits a
|
|
757
949
|
// classified response.failed (bridge case "error") — never a truncated completion.
|
|
758
|
-
if (chunk.error) {
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
yield* flushToolCalls();
|
|
763
|
-
yield {
|
|
764
|
-
type: "error",
|
|
765
|
-
message,
|
|
766
|
-
...(typeof err?.code === "string" ? { code: err.code } : {}),
|
|
767
|
-
...(typeof err?.type === "string" ? { errorType: err.type } : {}),
|
|
768
|
-
...(isCyberPolicyCode(err?.code)
|
|
769
|
-
? { status: 400 }
|
|
770
|
-
: typeof err?.status === "number" && Number.isInteger(err.status)
|
|
771
|
-
? { status: err.status }
|
|
772
|
-
: {}),
|
|
773
|
-
};
|
|
774
|
-
return "terminate";
|
|
950
|
+
if (chunk.error !== undefined && chunk.error !== null) {
|
|
951
|
+
const event = upstreamErrorEvent(chunk.error, pendingUsage);
|
|
952
|
+
debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
|
|
953
|
+
return yield* terminateWithError(event);
|
|
775
954
|
}
|
|
776
955
|
|
|
777
956
|
if (chunk.usage) {
|
|
@@ -781,17 +960,36 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
781
960
|
pendingUsage = usageFromOpenAIChat(chunk.usage as Record<string, unknown>);
|
|
782
961
|
}
|
|
783
962
|
|
|
784
|
-
const choices = chunk.choices
|
|
785
|
-
if (
|
|
963
|
+
const choices = chunk.choices;
|
|
964
|
+
if (choices === undefined) return "continue";
|
|
965
|
+
if (!Array.isArray(choices)) {
|
|
966
|
+
return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
|
|
967
|
+
}
|
|
968
|
+
if (choices.length === 0) return "continue";
|
|
969
|
+
const rawChoice = choices[0];
|
|
970
|
+
if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
|
|
971
|
+
return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
|
|
972
|
+
}
|
|
973
|
+
const choice = rawChoice as {
|
|
974
|
+
delta?: Record<string, unknown>;
|
|
975
|
+
finish_reason?: string;
|
|
976
|
+
error?: unknown;
|
|
977
|
+
};
|
|
978
|
+
if (choice.finish_reason === "error") {
|
|
979
|
+
const event = upstreamErrorEvent(choice.error, pendingUsage);
|
|
980
|
+
debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
|
|
981
|
+
return yield* terminateWithError(event);
|
|
982
|
+
}
|
|
786
983
|
// Observe the terminator BEFORE the delta guard: a finish-only chunk (finish_reason set,
|
|
787
984
|
// no delta) is a graceful close and must record finishReason even though we skip it below.
|
|
788
|
-
if (typeof
|
|
789
|
-
finishReason =
|
|
985
|
+
if (typeof choice.finish_reason === "string" && choice.finish_reason) {
|
|
986
|
+
finishReason = choice.finish_reason;
|
|
790
987
|
}
|
|
791
|
-
const delta =
|
|
988
|
+
const delta = choice.delta;
|
|
792
989
|
if (delta) {
|
|
793
|
-
|
|
794
|
-
|
|
990
|
+
const reasoningText = reasoningTextFrom(delta);
|
|
991
|
+
if (reasoningText !== undefined) {
|
|
992
|
+
yield { type: "reasoning_raw_delta", text: reasoningText };
|
|
795
993
|
}
|
|
796
994
|
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
797
995
|
sawUserFacingOutput = true;
|
|
@@ -839,7 +1037,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
839
1037
|
|
|
840
1038
|
// Any non-empty finish_reason ends the generation: flush assembled tool calls as
|
|
841
1039
|
// atomic sequences (covers "tool_calls" AND providers that close tool turns with "stop").
|
|
842
|
-
if (typeof
|
|
1040
|
+
if (typeof choice.finish_reason === "string" && choice.finish_reason) {
|
|
843
1041
|
yield* flushToolCalls();
|
|
844
1042
|
}
|
|
845
1043
|
return "continue";
|
|
@@ -918,11 +1116,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
918
1116
|
}
|
|
919
1117
|
yield* flushToolCalls();
|
|
920
1118
|
// Graceful close that omitted [DONE] but delivered finish_reason and/or answer text.
|
|
921
|
-
const stopReason = finishReason
|
|
922
|
-
? "max_tokens"
|
|
923
|
-
: finishReason === "content_filter"
|
|
924
|
-
? "content_filter"
|
|
925
|
-
: undefined;
|
|
1119
|
+
const stopReason = stopReasonFor(finishReason);
|
|
926
1120
|
yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
|
|
927
1121
|
} catch (error) {
|
|
928
1122
|
if (isTranslatorBudgetExceededError(error)
|
|
@@ -940,7 +1134,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
940
1134
|
throw error;
|
|
941
1135
|
} finally {
|
|
942
1136
|
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
943
|
-
|
|
1137
|
+
closeToolCalls();
|
|
944
1138
|
reader.releaseLock();
|
|
945
1139
|
}
|
|
946
1140
|
},
|
|
@@ -950,34 +1144,40 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
950
1144
|
const responseBytes = new TextEncoder().encode(JSON.stringify(json)).byteLength;
|
|
951
1145
|
budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
|
|
952
1146
|
try {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const code = typeof upstreamError.code === "string" ? upstreamError.code : undefined;
|
|
957
|
-
const errorType = typeof upstreamError.type === "string" ? upstreamError.type : undefined;
|
|
958
|
-
const status = isCyberPolicyCode(code)
|
|
959
|
-
? 400
|
|
960
|
-
: typeof upstreamError.status === "number" && Number.isInteger(upstreamError.status)
|
|
961
|
-
? upstreamError.status
|
|
962
|
-
: undefined;
|
|
1147
|
+
const payload = unwrapChatCompletionPayload(json);
|
|
1148
|
+
const usage = usageFromOpenAIChat(payload.usage as Record<string, unknown> | undefined);
|
|
1149
|
+
if (json.success === false && payload.error === undefined) {
|
|
963
1150
|
return [{
|
|
964
1151
|
type: "error",
|
|
965
|
-
message,
|
|
966
|
-
...(
|
|
967
|
-
...(errorType !== undefined ? { errorType } : {}),
|
|
968
|
-
...(status !== undefined ? { status } : {}),
|
|
1152
|
+
message: "upstream reported failure without an error payload",
|
|
1153
|
+
...(usage ? { usage } : {}),
|
|
969
1154
|
}];
|
|
970
1155
|
}
|
|
1156
|
+
if (payload.error !== undefined && payload.error !== null) {
|
|
1157
|
+
return [upstreamErrorEvent(payload.error, usage)];
|
|
1158
|
+
}
|
|
971
1159
|
|
|
972
1160
|
const events: AdapterEvent[] = [];
|
|
973
|
-
const choices =
|
|
974
|
-
|
|
975
|
-
|
|
1161
|
+
const choices = payload.choices as {
|
|
1162
|
+
message?: Record<string, unknown>;
|
|
1163
|
+
finish_reason?: unknown;
|
|
1164
|
+
error?: OpenAIChatError;
|
|
1165
|
+
}[] | undefined;
|
|
1166
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
1167
|
+
return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
|
|
1168
|
+
}
|
|
1169
|
+
const rawChoice = choices[0];
|
|
1170
|
+
if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
|
|
1171
|
+
return [invalidChoicesEvent(usage)];
|
|
976
1172
|
}
|
|
1173
|
+
const choice = rawChoice;
|
|
1174
|
+
if (choice.finish_reason === "error") return [upstreamErrorEvent(choice.error, usage)];
|
|
1175
|
+
if (!choice.message) return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
|
|
977
1176
|
|
|
978
|
-
const msg =
|
|
979
|
-
|
|
980
|
-
|
|
1177
|
+
const msg = choice.message;
|
|
1178
|
+
const reasoningText = reasoningTextFrom(msg);
|
|
1179
|
+
if (reasoningText !== undefined) {
|
|
1180
|
+
events.push({ type: "reasoning_raw_delta", text: reasoningText });
|
|
981
1181
|
}
|
|
982
1182
|
if (typeof msg.content === "string") {
|
|
983
1183
|
events.push({ type: "text_delta", text: msg.content });
|
|
@@ -990,10 +1190,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
990
1190
|
events.push({ type: "tool_call_end" });
|
|
991
1191
|
}
|
|
992
1192
|
}
|
|
993
|
-
const
|
|
1193
|
+
const stopReason = stopReasonFor(choice.finish_reason);
|
|
994
1194
|
events.push({
|
|
995
1195
|
type: "done",
|
|
996
|
-
usage
|
|
1196
|
+
usage,
|
|
1197
|
+
...(stopReason ? { stopReason } : {}),
|
|
997
1198
|
});
|
|
998
1199
|
retainTranslatedEventBatch(events, budget);
|
|
999
1200
|
return events;
|