@bitkyc08/opencodex 2.60.0 → 2.61.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/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-CH6C5H7x.js +50 -0
- package/gui/dist/assets/Tray-CLZh48fM.js +1 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/index-wpTOyepx.js +86 -0
- package/gui/dist/assets/usage-companion-chart-a0N58rRI.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +5 -1
- package/src/adapters/anthropic.ts +16 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +10 -2
- package/src/adapters/command-code.ts +2 -1
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +13 -3
- package/src/adapters/cursor.ts +11 -2
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin.ts +75 -23
- package/src/adapters/google-antigravity-wire.ts +5 -2
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google.ts +29 -5
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat.ts +18 -57
- package/src/adapters/openai-responses/passthrough.ts +2 -0
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +10 -0
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +43 -4
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/index.ts +200 -61
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-listener.ts +2 -5
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/provider-models.ts +24 -15
- package/src/codex/catalog/retained-sync.ts +2 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/history-provider.ts +12 -1
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +52 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +87 -44
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +5 -41
- package/src/codex/shim-templates.ts +29 -3
- package/src/companion/settings.ts +132 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/load-degrade.ts +34 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/schema/config-schema.ts +27 -1
- package/src/config/schema/leaf-validators.ts +47 -0
- package/src/config.ts +1 -1
- package/src/generated/compatibility-version.json +418 -174
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +124 -18
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +49 -11
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug.ts +40 -0
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +59 -14
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/index.ts +24 -1
- package/src/oauth/login-cli.ts +80 -29
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/key-failover.ts +31 -1
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +124 -37
- package/src/providers/quota/vendor-probes-key.ts +37 -22
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +9 -4
- package/src/providers/registry/entries-extended.ts +29 -4
- package/src/providers/registry/model-seeds.ts +47 -10
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/schema.ts +3 -0
- package/src/router.ts +17 -2
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +26 -0
- package/src/server/chat-completions.ts +55 -2
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +55 -17
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +56 -10
- package/src/server/index/spend-ledger-lifecycle.ts +34 -8
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index.ts +21 -28
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +6 -0
- package/src/server/management/shared.ts +18 -1
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +8 -9
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/relay.ts +19 -2
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +114 -0
- package/src/server/request-metrics.ts +92 -30
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +11 -0
- package/src/server/responses/core-opaque-recovery.ts +90 -0
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +14 -1
- package/src/server/responses/passthrough-dispatch.ts +179 -35
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/request-prepare.ts +42 -1
- package/src/server/responses/request-send-budget.ts +12 -0
- package/src/server/responses/request-transport.ts +24 -4
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +4 -1
- package/src/server/search.ts +25 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +1 -1
- package/src/types/config.ts +37 -0
- package/src/types/provider.ts +73 -0
- package/src/types/request.ts +28 -2
- package/src/types/tools.ts +19 -0
- package/src/types.ts +3 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/web-search/alpha-search.ts +21 -1
- package/gui/dist/assets/index-BTuCbqQd.css +0 -1
- package/gui/dist/assets/index-DoBVdPHP.js +0 -134
package/src/adapters/devin.ts
CHANGED
|
@@ -192,6 +192,35 @@ async function resolveWireModelUid(
|
|
|
192
192
|
*/
|
|
193
193
|
export const resolveWireModelUidForTests = resolveWireModelUid;
|
|
194
194
|
|
|
195
|
+
const positiveTokenCount = (value: unknown): number | undefined =>
|
|
196
|
+
typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Read a per-model token count for the exact UID selected for this turn.
|
|
200
|
+
*
|
|
201
|
+
* Tries the selected UID and then its collapsed base id, preferring the
|
|
202
|
+
* canonical spelling and accepting dotted or case-folded saved hints — the same
|
|
203
|
+
* normalization the inference request applies to the model id. Where several
|
|
204
|
+
* spellings match one id, the smallest wins: a ceiling stated twice is
|
|
205
|
+
* satisfied by the lower statement.
|
|
206
|
+
*/
|
|
207
|
+
function devinModelTokenHint(
|
|
208
|
+
record: Record<string, number> | undefined,
|
|
209
|
+
modelUid: string,
|
|
210
|
+
): number | undefined {
|
|
211
|
+
if (!record) return undefined;
|
|
212
|
+
for (const id of [modelUid, collapseDevinModelUid(modelUid)]) {
|
|
213
|
+
const exact = Object.hasOwn(record, id) ? positiveTokenCount(record[id]) : undefined;
|
|
214
|
+
if (exact !== undefined) return exact;
|
|
215
|
+
const matches = Object.entries(record)
|
|
216
|
+
.filter(([key]) => normalizeDevinModelId(key).toLowerCase() === id.toLowerCase())
|
|
217
|
+
.map(([, value]) => positiveTokenCount(value))
|
|
218
|
+
.filter((value): value is number => value !== undefined);
|
|
219
|
+
if (matches.length > 0) return Math.min(...matches);
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
195
224
|
/**
|
|
196
225
|
* Resolve the INPUT ceiling for the exact UID selected for this turn. Catalog
|
|
197
226
|
* ClientModelConfig #18 and CompletionConfiguration #3 both carry input tokens;
|
|
@@ -204,33 +233,50 @@ function resolveDevinMaxInputTokens(
|
|
|
204
233
|
modelUid: string,
|
|
205
234
|
liveWindow?: number,
|
|
206
235
|
): number | undefined {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
if (!record) return undefined;
|
|
212
|
-
for (const id of [modelUid, baseId]) {
|
|
213
|
-
// Prefer the canonical spelling; retain dotted/case-folded saved hints,
|
|
214
|
-
// matching the model-id normalization used for the inference request.
|
|
215
|
-
const exact = Object.hasOwn(record, id) ? positive(record[id]) : undefined;
|
|
216
|
-
if (exact !== undefined) return exact;
|
|
217
|
-
const matches = Object.entries(record)
|
|
218
|
-
.filter(([key]) => normalizeDevinModelId(key).toLowerCase() === id.toLowerCase())
|
|
219
|
-
.map(([, value]) => positive(value))
|
|
220
|
-
.filter((value): value is number => value !== undefined);
|
|
221
|
-
if (matches.length > 0) return Math.min(...matches);
|
|
222
|
-
}
|
|
223
|
-
return undefined;
|
|
224
|
-
};
|
|
225
|
-
const contextHint = configured(provider.modelContextWindows) ?? positive(provider.contextWindow);
|
|
226
|
-
const inputHint = configured(provider.modelMaxInputTokens);
|
|
227
|
-
const ceilings = [positive(liveWindow), contextHint, inputHint]
|
|
236
|
+
const contextHint = devinModelTokenHint(provider.modelContextWindows, modelUid)
|
|
237
|
+
?? positiveTokenCount(provider.contextWindow);
|
|
238
|
+
const inputHint = devinModelTokenHint(provider.modelMaxInputTokens, modelUid);
|
|
239
|
+
const ceilings = [positiveTokenCount(liveWindow), contextHint, inputHint]
|
|
228
240
|
.filter((value): value is number => value !== undefined);
|
|
229
241
|
return ceilings.length > 0 ? Math.min(...ceilings) : undefined;
|
|
230
242
|
}
|
|
231
243
|
|
|
232
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* Resolve the OUTPUT ceiling for this turn, highest authority first:
|
|
246
|
+
*
|
|
247
|
+
* 1. the caller's explicit `max_output_tokens`, forwarded unchanged — an
|
|
248
|
+
* explicit cap is a request, so a small one is never widened into a
|
|
249
|
+
* configured larger one;
|
|
250
|
+
* 2. the configured per-model cap (`modelMaxOutputTokens`), read through the
|
|
251
|
+
* same UID-aware hint lookup the input ceiling uses;
|
|
252
|
+
* 3. the provider-wide `defaultMaxOutputTokens`;
|
|
253
|
+
* 4. undefined, which leaves the cloud-direct encoder's own 8192 fallback in
|
|
254
|
+
* place for a provider that configured nothing.
|
|
255
|
+
*
|
|
256
|
+
* This is NOT the history ceiling, and the two must not collapse into one
|
|
257
|
+
* number. CompletionConfiguration #2 is the output cap and #3 is the context
|
|
258
|
+
* window, so feeding a context window into this resolver would ask Cognition to
|
|
259
|
+
* generate a whole window's worth of output. Nothing here reads
|
|
260
|
+
* `contextWindow` or `modelContextWindows` for that reason.
|
|
261
|
+
*
|
|
262
|
+
* Step 1 keeps the caller's raw value rather than `positiveTokenCount`: the
|
|
263
|
+
* inbound parser owns what a caller may send, and re-filtering here would
|
|
264
|
+
* silently promote a rejected value to a configured cap the caller never asked
|
|
265
|
+
* for.
|
|
266
|
+
*/
|
|
267
|
+
function resolveDevinMaxOutputTokens(
|
|
268
|
+
provider: OcxProviderConfig,
|
|
269
|
+
modelUid: string,
|
|
270
|
+
requested: number | undefined,
|
|
271
|
+
): number | undefined {
|
|
272
|
+
if (typeof requested === "number") return requested;
|
|
273
|
+
return devinModelTokenHint(provider.modelMaxOutputTokens, modelUid)
|
|
274
|
+
?? positiveTokenCount(provider.defaultMaxOutputTokens);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Pure test seams; runtime uses the same resolvers immediately before dispatch. */
|
|
233
278
|
export const resolveDevinMaxInputTokensForTests = resolveDevinMaxInputTokens;
|
|
279
|
+
export const resolveDevinMaxOutputTokensForTests = resolveDevinMaxOutputTokens;
|
|
234
280
|
|
|
235
281
|
export class DevinMissingCredentialError extends Error {
|
|
236
282
|
constructor() {
|
|
@@ -285,6 +331,9 @@ function mapOcxContentToWire(content: string | OcxContentPart[] | undefined): st
|
|
|
285
331
|
for (const part of content) {
|
|
286
332
|
if (part.type === "text" && part.text) {
|
|
287
333
|
out.push({ type: "text", text: part.text });
|
|
334
|
+
} else if (part.type === "document") {
|
|
335
|
+
// No Devin document field; the marker keeps the turn from disappearing entirely.
|
|
336
|
+
out.push({ type: "text", text: part.text });
|
|
288
337
|
} else if (part.type === "image") {
|
|
289
338
|
const m = part.imageUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
290
339
|
if (m) out.push({ type: "image", mimeType: m[1]!, base64Data: m[2]! });
|
|
@@ -590,6 +639,9 @@ export function createDevinAdapter(
|
|
|
590
639
|
const maxInputTokens = resolveDevinMaxInputTokens(
|
|
591
640
|
provider, modelUid, catalog?.byUid.get(modelUid)?.contextWindow,
|
|
592
641
|
);
|
|
642
|
+
const maxOutputTokens = resolveDevinMaxOutputTokens(
|
|
643
|
+
provider, modelUid, parsed.options.maxOutputTokens,
|
|
644
|
+
);
|
|
593
645
|
// The reset-retry wrapper waits out a 429 that states its own recovery
|
|
594
646
|
// delay ("limit will reset in 35 seconds") and replays the identical
|
|
595
647
|
// request — but only while zero events have been yielded, so a
|
|
@@ -606,7 +658,7 @@ export function createDevinAdapter(
|
|
|
606
658
|
// input hint used to force every model through the 128k default.
|
|
607
659
|
completionOpts: {
|
|
608
660
|
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
|
609
|
-
...(
|
|
661
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
610
662
|
...(typeof parsed.options.temperature === "number" ? { temperature: parsed.options.temperature } : {}),
|
|
611
663
|
...(typeof parsed.options.topP === "number" ? { topP: parsed.options.topP } : {}),
|
|
612
664
|
},
|
|
@@ -45,8 +45,11 @@ function firstUserText(parsed: OcxParsedRequest): string | undefined {
|
|
|
45
45
|
for (const msg of parsed.context.messages) {
|
|
46
46
|
if (msg.role !== "user") continue;
|
|
47
47
|
if (typeof msg.content === "string") return msg.content;
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// A document part carries text too: ignoring it left a document-only opening turn with no
|
|
49
|
+
// anchor, which silently downgrades the deterministic session id to a random one.
|
|
50
|
+
const first = (msg.content as OcxContentPart[])
|
|
51
|
+
.find(p => (p.type === "text" || p.type === "document") && typeof p.text === "string");
|
|
52
|
+
if (first && (first.type === "text" || first.type === "document")) return first.text;
|
|
50
53
|
}
|
|
51
54
|
return undefined;
|
|
52
55
|
}
|
|
@@ -66,7 +66,13 @@ function classifyGoogle(label: string, status: number | undefined, enumStatus: s
|
|
|
66
66
|
if (status === 401 || enumStatus === "UNAUTHENTICATED" || lower.includes("unauthenticated") || lower.includes("invalid authentication") || lower.includes("expired")) {
|
|
67
67
|
return `${label} authentication failed`;
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
// Keep Google's explicit enum in the normalized text. Responses/combo handling receives
|
|
70
|
+
// only this string, so dropping it would let location wording override the authoritative
|
|
71
|
+
// permission reason during downstream classification.
|
|
72
|
+
if (enumStatus === "PERMISSION_DENIED") {
|
|
73
|
+
return `${label} access denied (PERMISSION_DENIED)`;
|
|
74
|
+
}
|
|
75
|
+
if (status === 403 || lower.includes("permission_denied") || lower.includes("permission denied") || lower.includes("access denied")) {
|
|
70
76
|
return `${label} access denied`;
|
|
71
77
|
}
|
|
72
78
|
// Google rejects unsupported geographic / datacenter locations with HTTP 400
|
package/src/adapters/google.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
OcxUsage,
|
|
16
16
|
} from "../types";
|
|
17
17
|
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
|
|
18
|
+
import type { OcxTool } from "../types";
|
|
18
19
|
import { contentPartsToText, parseDataUrl } from "./image";
|
|
19
20
|
import { getVertexAccessToken } from "../lib/gcp-adc";
|
|
20
21
|
import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
|
|
@@ -346,6 +347,13 @@ function messagesToGeminiFormat(
|
|
|
346
347
|
parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[video: ${p.videoUrl}]` });
|
|
347
348
|
continue;
|
|
348
349
|
}
|
|
350
|
+
if (p.type === "document") {
|
|
351
|
+
// Gemini takes document bytes through the same inline_data part as images and
|
|
352
|
+
// video. The marker on the part is the fallback for wires without one, not this
|
|
353
|
+
// wire's best effort (#5212).
|
|
354
|
+
parts.push({ inline_data: { mime_type: p.mediaType, data: p.data } });
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
349
357
|
// Drop empty/malformed text instead of emitting `{ text: "" }` or a bare `{}` part.
|
|
350
358
|
const textPart = geminiTextPart(p.text);
|
|
351
359
|
if (textPart) parts.push(textPart);
|
|
@@ -465,9 +473,7 @@ function messagesToGeminiFormat(
|
|
|
465
473
|
|
|
466
474
|
function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
|
|
467
475
|
if (!parsed.context.tools?.length) return undefined;
|
|
468
|
-
const tools =
|
|
469
|
-
? parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools))
|
|
470
|
-
: parsed.context.tools;
|
|
476
|
+
const tools = advertisedGeminiTools(parsed);
|
|
471
477
|
if (tools.length === 0) return undefined;
|
|
472
478
|
return [{
|
|
473
479
|
functionDeclarations: tools.map(t => ({
|
|
@@ -478,19 +484,37 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
|
|
|
478
484
|
}];
|
|
479
485
|
}
|
|
480
486
|
|
|
487
|
+
/** The declarations this request actually advertises, after any allowed-tools filter. */
|
|
488
|
+
function advertisedGeminiTools(parsed: OcxParsedRequest): readonly OcxTool[] {
|
|
489
|
+
const declared = parsed.context.tools ?? [];
|
|
490
|
+
return isAllowedToolChoice(parsed.options.toolChoice)
|
|
491
|
+
? declared.filter(toolChoiceToolPredicate(parsed.options.toolChoice, declared))
|
|
492
|
+
: declared;
|
|
493
|
+
}
|
|
494
|
+
|
|
481
495
|
/**
|
|
482
496
|
* Client tool_choice enforcement on the wire. The catalog nudge states the same contract in
|
|
483
497
|
* prose, but without functionCallingConfig the model is free to ignore it. "auto" stays absent
|
|
484
498
|
* so the common case is byte-identical. The allowedTools variant already filters the
|
|
485
499
|
* declarations in toolsToGeminiFormat; only its "required" half needs a wire mode.
|
|
500
|
+
*
|
|
501
|
+
* A caller that declares strict tools is asking for its argument schemas to be enforced, and
|
|
502
|
+
* Gemini expresses that as VALIDATED. The mode existed and was plumbed end to end, but was only
|
|
503
|
+
* ever reachable by matching a model name, so a strict declaration arrived as an ordinary
|
|
504
|
+
* unvalidated AUTO turn and the response looked the same either way (#5210). VALIDATED replaces
|
|
505
|
+
* AUTO only: ANY and NONE are stronger constraints the caller asked for explicitly, and
|
|
506
|
+
* overwriting either of them would lose the choice this function exists to enforce.
|
|
486
507
|
*/
|
|
487
508
|
function toolChoiceToGeminiToolConfig(parsed: OcxParsedRequest): Record<string, unknown> | undefined {
|
|
488
509
|
const choice = parsed.options.toolChoice;
|
|
489
|
-
|
|
510
|
+
const validated = advertisedGeminiTools(parsed).some(t => t.strict === true)
|
|
511
|
+
? { functionCallingConfig: { mode: "VALIDATED" } }
|
|
512
|
+
: undefined;
|
|
513
|
+
if (!choice || choice === "auto") return validated;
|
|
490
514
|
if (choice === "none") return { functionCallingConfig: { mode: "NONE" } };
|
|
491
515
|
if (choice === "required") return { functionCallingConfig: { mode: "ANY" } };
|
|
492
516
|
if (isAllowedToolChoice(choice)) {
|
|
493
|
-
return choice.mode === "required" ? { functionCallingConfig: { mode: "ANY" } } :
|
|
517
|
+
return choice.mode === "required" ? { functionCallingConfig: { mode: "ANY" } } : validated;
|
|
494
518
|
}
|
|
495
519
|
return {
|
|
496
520
|
functionCallingConfig: {
|
package/src/adapters/image.ts
CHANGED
|
@@ -18,6 +18,9 @@ export function parseDataUrl(url: string): { mediaType: string; base64: string }
|
|
|
18
18
|
*/
|
|
19
19
|
export function contentPartsToText(content: string | OcxContentPart[]): string {
|
|
20
20
|
if (typeof content === "string") return content;
|
|
21
|
-
|
|
21
|
+
// A document carries its own marker, so this wire states the attachment instead of
|
|
22
|
+
// mislabelling it as a video.
|
|
23
|
+
const text = content.map(p =>
|
|
24
|
+
p.type === "text" || p.type === "document" ? p.text : p.type === "image" ? "[image]" : "[video]").join("");
|
|
22
25
|
return text || "[image]";
|
|
23
26
|
}
|
|
@@ -1,31 +1,45 @@
|
|
|
1
1
|
import type { ProviderAdapter } from "./base";
|
|
2
2
|
import { untranslatedInputMediaMessage, untranslatedResponsesInputMedia } from "../responses/input-media";
|
|
3
|
+
import { unrepresentableDeclaration } from "./declaration-carrier";
|
|
4
|
+
import type { AdapterWire } from "./registry";
|
|
5
|
+
import type { OcxParsedRequest } from "../types";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Refuse unrepresentable input at the final translated-adapter boundary. The registry
|
|
6
9
|
* applies this after wire resolution; Responses passthrough (including Azure) opts
|
|
7
10
|
* out because it uses the original body rather than the lossy normalized content.
|
|
11
|
+
*
|
|
12
|
+
* Two of the three checks are wire-scoped, and that is the point. A constraint the normalized
|
|
13
|
+
* request CAN carry — a caller restriction on a tool, an attached document's bytes — still has
|
|
14
|
+
* to reach a wire that can express it. Leaving that to each adapter means an adapter that never
|
|
15
|
+
* learned about the carrier rebuilds without it and answers normally, so the allowlist in
|
|
16
|
+
* `declaration-carrier.ts` is default-deny and this is the one place every registered adapter
|
|
17
|
+
* passes through.
|
|
8
18
|
*/
|
|
9
|
-
export function withInputMediaGuard<T extends ProviderAdapter>(adapter: T): T {
|
|
19
|
+
export function withInputMediaGuard<T extends ProviderAdapter>(adapter: T, wire: AdapterWire): T {
|
|
20
|
+
const refusal = (parsed: OcxParsedRequest): string | undefined => {
|
|
21
|
+
const kind = untranslatedResponsesInputMedia(parsed._rawBody);
|
|
22
|
+
return kind ? untranslatedInputMediaMessage(kind) : unrepresentableDeclaration(parsed, wire);
|
|
23
|
+
};
|
|
10
24
|
const build = adapter.buildRequest.bind(adapter);
|
|
11
25
|
adapter.buildRequest = (parsed, incoming) => {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
26
|
+
const message = refusal(parsed);
|
|
27
|
+
if (message) throw new Error(message);
|
|
14
28
|
return build(parsed, incoming);
|
|
15
29
|
};
|
|
16
30
|
|
|
17
31
|
const runTurn = adapter.runTurn?.bind(adapter);
|
|
18
32
|
if (runTurn) {
|
|
19
33
|
adapter.runTurn = async (parsed, incoming, emit) => {
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
34
|
+
const message = refusal(parsed);
|
|
35
|
+
if (message) {
|
|
22
36
|
emit({
|
|
23
37
|
type: "error",
|
|
24
38
|
status: 400,
|
|
25
39
|
errorType: "invalid_request_error",
|
|
26
40
|
code: "unsupported_input_modality",
|
|
27
41
|
retryable: false,
|
|
28
|
-
message
|
|
42
|
+
message,
|
|
29
43
|
});
|
|
30
44
|
return;
|
|
31
45
|
}
|
|
@@ -37,9 +51,7 @@ export function withInputMediaGuard<T extends ProviderAdapter>(adapter: T): T {
|
|
|
37
51
|
if (localTerminal) {
|
|
38
52
|
// This hook is outside the builder's error catch. Decline its success shortcut;
|
|
39
53
|
// the ordinary buildRequest path then returns the established client-safe 400.
|
|
40
|
-
adapter.localTerminal = parsed =>
|
|
41
|
-
? undefined
|
|
42
|
-
: localTerminal(parsed);
|
|
54
|
+
adapter.localTerminal = parsed => refusal(parsed) ? undefined : localTerminal(parsed);
|
|
43
55
|
}
|
|
44
56
|
return adapter;
|
|
45
57
|
}
|
|
@@ -12,14 +12,15 @@ import type { KiroHistoryEntry } from "./wire";
|
|
|
12
12
|
|
|
13
13
|
export function userContentText(content: string | OcxContentPart[]): string {
|
|
14
14
|
if (typeof content === "string") return content;
|
|
15
|
-
|
|
15
|
+
// A document carries its own marker: dropping it built an empty user turn that Kiro rejects.
|
|
16
|
+
return content.map(p => (p.type === "text" || p.type === "document" ? p.text : "")).filter(Boolean).join("\n");
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function usageContentText(content: string | OcxContentPart[]): string {
|
|
19
20
|
if (typeof content === "string") return content;
|
|
20
21
|
return content
|
|
21
22
|
.map(p => {
|
|
22
|
-
if (p.type === "text") return p.text;
|
|
23
|
+
if (p.type === "text" || p.type === "document") return p.text;
|
|
23
24
|
if (p.type === "image") return `[image:${p.detail ?? "auto"}]`;
|
|
24
25
|
return "";
|
|
25
26
|
})
|
|
@@ -9,7 +9,7 @@ function contentText(content: string | OcxContentPart[]): string {
|
|
|
9
9
|
if (typeof content === "string") return content;
|
|
10
10
|
return content
|
|
11
11
|
.map(part => {
|
|
12
|
-
if (part.type === "text") return part.text;
|
|
12
|
+
if (part.type === "text" || part.type === "document") return part.text;
|
|
13
13
|
if (part.type === "image") return `[image:${part.detail ?? "auto"}]`;
|
|
14
14
|
return "";
|
|
15
15
|
})
|
|
@@ -262,6 +262,12 @@ function contentToNative(
|
|
|
262
262
|
text += part.text;
|
|
263
263
|
continue;
|
|
264
264
|
}
|
|
265
|
+
// No Ollama document carrier: keep the marker rather than falling through to the image
|
|
266
|
+
// branch below, which would read a nonexistent imageUrl.
|
|
267
|
+
if (part.type === "document") {
|
|
268
|
+
text += part.text;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
265
271
|
// Ollama's native /api/chat message shape carries `images: string[]` and has no video
|
|
266
272
|
// counterpart, so a video part is refused rather than silently dropped or mis-sent as an image.
|
|
267
273
|
if (part.type === "video") throw new Error(`ollama-native cannot send video content in ${label}`);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The role a `developer` message carries on the Chat wire, decided in one place for both the
|
|
5
|
+
* translated adapter and the native passthrough.
|
|
6
|
+
*
|
|
7
|
+
* `developer` belongs to the Chat Completions role set, but not every OpenAI-compatible gateway
|
|
8
|
+
* accepts it: one that does not answers `400 role 'developer' is not allowed` and the turn never
|
|
9
|
+
* starts. #5213 removed a hostname test that decided the role, which was right — a gateway
|
|
10
|
+
* proxying OpenAI accepts the role and the hostname cannot say so.
|
|
11
|
+
*
|
|
12
|
+
* `foldDeveloperRoleToSystem` is tri-state and only two of its states say anything about the
|
|
13
|
+
* destination: `true` records an upstream known to reject the role, `false` one known to accept
|
|
14
|
+
* it, and absent means nobody has recorded either. Returning `undefined` for the absent state
|
|
15
|
+
* instead of a role keeps the recorded fact separate from the default a route applies to
|
|
16
|
+
* silence, which is what lets the native route honour the first without inheriting the second.
|
|
17
|
+
*
|
|
18
|
+
* This decides the role and only the role. Which slot the message occupies is the caller's
|
|
19
|
+
* decision and must not change with this value.
|
|
20
|
+
*/
|
|
21
|
+
export function explicitChatDeveloperWireRole(
|
|
22
|
+
provider: OcxProviderConfig,
|
|
23
|
+
): "developer" | "system" | undefined {
|
|
24
|
+
if (provider.foldDeveloperRoleToSystem === undefined) return undefined;
|
|
25
|
+
return provider.foldDeveloperRoleToSystem ? "system" : "developer";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The translated route's role: the recorded one, or `system` when nothing is recorded.
|
|
30
|
+
*
|
|
31
|
+
* The unrecorded state folds because the two mistakes are not symmetrical. Sending `developer`
|
|
32
|
+
* to a destination that rejects it fails the request outside this repository, where no test here
|
|
33
|
+
* can reach it; sending `system` to one that would have accepted `developer` costs the role name
|
|
34
|
+
* and nothing else.
|
|
35
|
+
*/
|
|
36
|
+
export function translatedChatDeveloperWireRole(provider: OcxProviderConfig): "developer" | "system" {
|
|
37
|
+
return explicitChatDeveloperWireRole(provider) ?? "system";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Apply a recorded role to a caller-supplied `messages` array, leaving every other field of
|
|
42
|
+
* every message, and the order of all of them, exactly as they arrived.
|
|
43
|
+
*
|
|
44
|
+
* The native route forwards the caller's messages verbatim, so an operator who recorded that a
|
|
45
|
+
* destination rejects the role still sent `developer` there and the turn failed upstream with a
|
|
46
|
+
* 400. Only an explicit record changes anything here: with the key unset, or set to the role the
|
|
47
|
+
* message already carries, the same array reference is returned and the wire is byte-identical
|
|
48
|
+
* to the one the caller sent.
|
|
49
|
+
*/
|
|
50
|
+
export function applyExplicitChatDeveloperRole(messages: unknown, provider: OcxProviderConfig): unknown {
|
|
51
|
+
const role = explicitChatDeveloperWireRole(provider);
|
|
52
|
+
if (role === undefined || role === "developer" || !Array.isArray(messages)) return messages;
|
|
53
|
+
let rewritten = false;
|
|
54
|
+
const applied = messages.map(message => {
|
|
55
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) return message;
|
|
56
|
+
if ((message as { role?: unknown }).role !== "developer") return message;
|
|
57
|
+
rewritten = true;
|
|
58
|
+
return { ...(message as Record<string, unknown>), role };
|
|
59
|
+
});
|
|
60
|
+
return rewritten ? applied : messages;
|
|
61
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { isNativeOpenAIChatTarget, stripBracketedModelSuffix } from "./wire";
|
|
2
2
|
import { reasoningDetailSegmentForWire } from "./response-events";
|
|
3
|
+
import { translatedChatDeveloperWireRole } from "./developer-role";
|
|
3
4
|
import { isVolcengineArkPaygChatTarget } from "./tool-schema";
|
|
4
5
|
import { contentPartsToText } from "../image";
|
|
5
6
|
import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../empty-tool-output-annotation";
|
|
6
7
|
import { identifyRoutedModel } from "../identity";
|
|
7
8
|
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "../tool-catalog-nudge";
|
|
8
|
-
import { registryEntryForProviderDestination } from "../../providers/registry";
|
|
9
9
|
import { peekReasoningForCall } from "../../responses/reasoning-replay-cache";
|
|
10
|
-
import
|
|
10
|
+
import { inlineDocumentDataUrl } from "../../responses/inline-document";
|
|
11
|
+
import type { OcxAssistantMessage, OcxContentPart, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall } from "../../types";
|
|
11
12
|
import { modelInList, namespacedToolName } from "../../types";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -21,13 +22,6 @@ import { modelInList, namespacedToolName } from "../../types";
|
|
|
21
22
|
*/
|
|
22
23
|
const VIDEO_UNSUPPORTED_MARKER = "[video omitted: the translated Chat route has no video mapping]";
|
|
23
24
|
|
|
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
25
|
/**
|
|
32
26
|
* Chat-completions image_url parts for images carried inside a tool result (issue #888). role:"tool"
|
|
33
27
|
* content is text-only on every chat provider, so these ride in a follow-up user message instead of
|
|
@@ -121,21 +115,26 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
121
115
|
};
|
|
122
116
|
|
|
123
117
|
const nativeOpenAI = isNativeOpenAIChatTarget(provider);
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
// Which role a developer message carries, and why the unrecorded state folds, is stated once
|
|
119
|
+
// in ./developer-role.ts and read from there by the native passthrough as well. Either way the
|
|
120
|
+
// message keeps the slot it arrived in — only the role changes, never the position.
|
|
121
|
+
const developerWireRole = translatedChatDeveloperWireRole(provider);
|
|
122
|
+
// A developer message keeps the slot it arrived in. Hoisting its text into the leading
|
|
123
|
+
// system block moved a mid-conversation instruction ahead of every turn it was written to
|
|
124
|
+
// follow, and the caller saw an ordinary answer either way (#5213). The Claude inbound mints
|
|
125
|
+
// chronological developer items for exactly this reason (#4161), so the two halves of the
|
|
126
|
+
// route were working against each other on every host but api.openai.com. Placement is now
|
|
127
|
+
// uniform; which ROLE that slot carries is decided separately below.
|
|
128
|
+
//
|
|
129
|
+
// One destination already had the chronological behaviour, keyed to a model and a registry
|
|
130
|
+
// id, because hoisting a newly appended reminder rewrites the reusable prompt prefix. That is
|
|
131
|
+
// a property of prompt-prefix caching rather than of that destination, and it is now what
|
|
132
|
+
// every destination gets.
|
|
128
133
|
const toolCatalogNudge = shouldInjectNonOpenAIToolCatalogNudge(provider)
|
|
129
134
|
? buildNonOpenAIToolCatalogNudgeForTools(context.tools, options.toolChoice)
|
|
130
135
|
: 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
136
|
const systemParts = [
|
|
137
137
|
...(context.systemPrompt ?? []),
|
|
138
|
-
...developerSystemParts,
|
|
139
138
|
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
140
139
|
];
|
|
141
140
|
if (systemParts.length > 0) {
|
|
@@ -152,20 +151,23 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
152
151
|
case "developer": {
|
|
153
152
|
const parts = typeof msg.content === "string" ? undefined : msg.content as OcxContentPart[];
|
|
154
153
|
const hasImages = parts?.some(p => p.type === "image") ?? false;
|
|
154
|
+
// A document has a structured counterpart on this wire, so it needs the parts array for
|
|
155
|
+
// the same reason an image does: flattening it to a string would drop the bytes.
|
|
156
|
+
const hasStructured = hasImages || (parts?.some(p => p.type === "document") ?? false);
|
|
155
157
|
let chatMsg: Record<string, unknown>;
|
|
156
|
-
if (msg.role === "developer" && !
|
|
157
|
-
if (!nativeOpenAI && !chronologicalSystem) break;
|
|
158
|
+
if (msg.role === "developer" && !hasStructured) {
|
|
158
159
|
const text = typeof msg.content === "string"
|
|
159
160
|
? msg.content
|
|
160
161
|
: parts!.map(p => (p as OcxTextContent).text).join("");
|
|
161
|
-
// A non-text timeline part (video, for example) serializes to nothing here.
|
|
162
|
-
//
|
|
163
|
-
//
|
|
162
|
+
// A non-text timeline part (video, for example) serializes to nothing here. The
|
|
163
|
+
// generic user path drops such a message, and emitting a content-free system
|
|
164
|
+
// message instead is rejected by some upstreams. Native OpenAI keeps its existing
|
|
165
|
+
// empty-developer wire, which is a separate question from placement.
|
|
164
166
|
if (!nativeOpenAI && text.length === 0) break;
|
|
165
|
-
chatMsg = { role:
|
|
167
|
+
chatMsg = { role: developerWireRole, content: text };
|
|
166
168
|
} else if (typeof msg.content === "string") {
|
|
167
169
|
chatMsg = { role: "user", content: msg.content };
|
|
168
|
-
} else if (!
|
|
170
|
+
} else if (!hasStructured) {
|
|
169
171
|
// A video part has no `text`, so joining it produced "" and the whole message
|
|
170
172
|
// was dropped: a video-only or text-plus-video turn vanished silently. OpenAI's
|
|
171
173
|
// Chat Completions wire has no video content part, so state the omission
|
|
@@ -183,13 +185,30 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
183
185
|
if (p.type === "image") {
|
|
184
186
|
return { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } };
|
|
185
187
|
}
|
|
188
|
+
// Chat Completions carries an attached document as a file part with inline bytes,
|
|
189
|
+
// the direct counterpart of the Anthropic document block the caller sent.
|
|
190
|
+
if (p.type === "document") {
|
|
191
|
+
return {
|
|
192
|
+
type: "file",
|
|
193
|
+
file: {
|
|
194
|
+
file_data: inlineDocumentDataUrl(p),
|
|
195
|
+
...(p.filename !== undefined ? { filename: p.filename } : {}),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
186
199
|
// Previously this produced { type: "text", text: undefined } for a video
|
|
187
200
|
// part — a malformed part, worse than a drop because it can fail upstream
|
|
188
201
|
// schema validation.
|
|
189
202
|
if (p.type === "video") return { type: "text", text: VIDEO_UNSUPPORTED_MARKER };
|
|
190
203
|
return { type: "text", text: (p as OcxTextContent).text };
|
|
191
204
|
});
|
|
192
|
-
|
|
205
|
+
// A developer message with images keeps the user-compatible shape it has always had on
|
|
206
|
+
// this wire. One carrying only a document has no such precedent, and demoting it would
|
|
207
|
+
// undo the role this adapter just finished preserving.
|
|
208
|
+
chatMsg = {
|
|
209
|
+
role: msg.role === "developer" && !hasImages ? developerWireRole : "user",
|
|
210
|
+
content: chatParts,
|
|
211
|
+
};
|
|
193
212
|
}
|
|
194
213
|
if (pendingToolCalls.length > 0) deferredBarrierMessages.push(chatMsg);
|
|
195
214
|
else out.push(chatMsg);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `parallel_tool_calls` value for a translated Chat request, or `undefined` to omit the key.
|
|
5
|
+
*
|
|
6
|
+
* A provider has three states here, not two, and the third is the default for every provider that
|
|
7
|
+
* never configured the knob. While the call site only branched on the two configured states, a
|
|
8
|
+
* caller's own explicit `parallel_tool_calls: false` reached the parser, was carried on
|
|
9
|
+
* `options.parallelToolCalls`, and was then dropped on the way to the wire — the upstream stayed
|
|
10
|
+
* free to emit concurrent calls and the response looked entirely normal (#5211).
|
|
11
|
+
*
|
|
12
|
+
* Only an explicit request-side `false` is forwarded in the unset state. `true` is already the
|
|
13
|
+
* upstream default, so emitting it would introduce the knob to strict OpenAI-compatible hosts
|
|
14
|
+
* that have never had to accept it, which is the reason the configured opt-out below omits the
|
|
15
|
+
* key rather than sending `false`.
|
|
16
|
+
*/
|
|
17
|
+
export function chatParallelToolCallsWireValue(
|
|
18
|
+
provider: OcxProviderConfig,
|
|
19
|
+
requested: boolean | undefined,
|
|
20
|
+
): boolean | undefined {
|
|
21
|
+
if (provider.parallelToolCalls === false) {
|
|
22
|
+
// NIM documents the Boolean defaulting to false and kimi rejects true; pin the wire bit so
|
|
23
|
+
// Codex cannot opt in via request.options. Other opted-out providers omit the field, but a
|
|
24
|
+
// self-hosted gateway that DOES honor it and keeps emitting parallel calls without it can opt
|
|
25
|
+
// in via pinParallelToolCallsFalse.
|
|
26
|
+
const pinned = provider.baseUrl === "https://integrate.api.nvidia.com/v1"
|
|
27
|
+
|| provider.pinParallelToolCallsFalse === true;
|
|
28
|
+
return pinned ? false : undefined;
|
|
29
|
+
}
|
|
30
|
+
if (provider.parallelToolCalls === true) return requested !== false;
|
|
31
|
+
return requested === false ? false : undefined;
|
|
32
|
+
}
|