@bitkyc08/opencodex 2.58.0 → 2.59.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/README.md +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +1 -1
- package/src/adapters/base.ts +11 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor.ts +42 -1
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +8 -1
- package/src/adapters/google.ts +23 -4
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +10 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +165 -24
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +54 -0
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +88 -20
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/history-provider.ts +4 -1
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +21 -0
- package/src/config/schema/leaf-validators.ts +64 -17
- package/src/generated/compatibility-version.json +235 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-extended.ts +96 -0
- package/src/providers/registry/model-seeds.ts +78 -21
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/server/auth-cors.ts +26 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +31 -6
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +10 -1
- package/src/server/responses/passthrough-dispatch.ts +113 -48
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +29 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +100 -3
- package/src/types/provider.ts +19 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
|
@@ -315,6 +315,13 @@ export const DEEPSEEK_VISION_PREVIEW_MODEL = "deepseek-v4-flash-vision-exp";
|
|
|
315
315
|
*/
|
|
316
316
|
export const COMMAND_CODE_IMAGE_MODELS = [
|
|
317
317
|
`deepseek/${DEEPSEEK_VISION_PREVIEW_MODEL}`,
|
|
318
|
+
// Probed 2026-09-18 through a running 2.58.0 proxy: a 3x3 random-color grid
|
|
319
|
+
// (180x180 PNG, six candidate colors) came back 9/9 correct both as a user
|
|
320
|
+
// message and as a tool_result, and the request logs show the route served
|
|
321
|
+
// the image natively — no vision-sidecar call in either window. #4505 asked
|
|
322
|
+
// for exactly this upstream probe before promoting the id. The sibling
|
|
323
|
+
// deepseek/deepseek-v4-flash route remains verified-negative above.
|
|
324
|
+
"deepseek/deepseek-v4.1-flash",
|
|
318
325
|
"gpt-5.6-luna",
|
|
319
326
|
"gpt-5.6-sol",
|
|
320
327
|
"MiniMaxAI/MiniMax-M3",
|
|
@@ -334,20 +341,19 @@ export const COMMAND_CODE_IMAGE_MODELS = [
|
|
|
334
341
|
/**
|
|
335
342
|
* Native image stays sourced from COMMAND_CODE_IMAGE_MODELS. Text-only routes
|
|
336
343
|
* sit beside that list so the catalog can still advertise sidecar coverage
|
|
337
|
-
* without claiming the gateway itself accepts a picture.
|
|
338
|
-
*
|
|
339
|
-
* The gateway-prefixed DeepSeek V4.1 Flash route has no verified native image
|
|
340
|
-
* support, so declaring it image-capable would hand it a picture it drops. A
|
|
341
|
-
* positive text-only declaration makes it a vision-sidecar consumer
|
|
344
|
+
* without claiming the gateway itself accepts a picture. A positive text-only
|
|
345
|
+
* declaration makes the route a vision-sidecar consumer
|
|
342
346
|
* (src/vision/eligibility.ts), so the catalog advertises image input on its
|
|
343
|
-
* behalf
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
+
* behalf — without claiming native vision — and modelInputModalities is
|
|
348
|
+
* per-key filled, so that reaches an existing install even when noVisionModels
|
|
349
|
+
* was persisted before the id joined a list.
|
|
350
|
+
*
|
|
351
|
+
* Empty as of 2026-09-18. Its only entry, deepseek/deepseek-v4.1-flash, moved
|
|
352
|
+
* to COMMAND_CODE_IMAGE_MODELS once the #4505-requested probe passed on both
|
|
353
|
+
* the user-message and tool-result paths (see the note at that entry). The
|
|
354
|
+
* mechanism stays for the next route that measures text-only.
|
|
347
355
|
*/
|
|
348
|
-
export const COMMAND_CODE_TEXT_ONLY_MODELS = [
|
|
349
|
-
"deepseek/deepseek-v4.1-flash",
|
|
350
|
-
] as const;
|
|
356
|
+
export const COMMAND_CODE_TEXT_ONLY_MODELS = [] as const;
|
|
351
357
|
export const COMMAND_CODE_MODEL_INPUT_MODALITIES: Record<string, ["text"] | ["text", "image"]> = {
|
|
352
358
|
...Object.fromEntries(COMMAND_CODE_IMAGE_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
|
|
353
359
|
...Object.fromEntries(COMMAND_CODE_TEXT_ONLY_MODELS.map(id => [id, ["text"] as ["text"]])),
|
|
@@ -439,9 +445,15 @@ export const deepseekReasoningMapFor = (modelId: string): Record<string, string>
|
|
|
439
445
|
// https://help.aliyun.com/en/model-studio/token-plan-quickstart
|
|
440
446
|
// 260909 refresh, re-probed against the live gateway (both regions, both tiers):
|
|
441
447
|
// https://github.com/oliver-mee/alibaba-token-plan-wiki (machine-readable catalog).
|
|
442
|
-
// glm-5.3
|
|
443
|
-
//
|
|
444
|
-
//
|
|
448
|
+
// 260918: glm-5.3 returns. The 260909 removal was correct at the time (the id
|
|
449
|
+
// 404'd on every plan key), but the gateway started serving glm-5.3 on 260917:
|
|
450
|
+
// it now appears on /models for global Team, global Personal, and CN Team, and
|
|
451
|
+
// answers a completion on a Personal key (probed 260918). Contract on the plan
|
|
452
|
+
// gateway: effort low/high/max (default max), thinking always-on (the gateway
|
|
453
|
+
// rejects enable_thinking:false with 400), 1M context, 131,072 max output,
|
|
454
|
+
// text-only input, strict json_schema accepted. glm-5.3-flash REMAINS OUT:
|
|
455
|
+
// still never served by the Token Plan gateway (docs.z.ai VLM id, not plan
|
|
456
|
+
// entitlement).
|
|
445
457
|
// The Beijing preset keeps the Personal Edition subset; non-chat ids (audio/image/
|
|
446
458
|
// video families) stay out: they answer only on async endpoints openai-chat cannot
|
|
447
459
|
// reach. deepseek-v4-pro-0813 is callable but NOT listed by /models, which is the
|
|
@@ -457,7 +469,7 @@ export const deepseekReasoningMapFor = (modelId: string): Record<string, string>
|
|
|
457
469
|
// drifting ones.
|
|
458
470
|
export const ALIBABA_TOKEN_PLAN_MODELS = [
|
|
459
471
|
"qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash",
|
|
460
|
-
"deepseek-v4-pro", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "glm-5.2",
|
|
472
|
+
"deepseek-v4-pro", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "glm-5.2", "glm-5.3",
|
|
461
473
|
];
|
|
462
474
|
export const ALIBABA_TOKEN_PLAN_QWEN_MODELS = [
|
|
463
475
|
"qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash",
|
|
@@ -474,6 +486,7 @@ export const ALIBABA_TOKEN_PLAN_INPUT_MODALITIES: Record<string, string[]> = {
|
|
|
474
486
|
// Vision probed on the plan gateway 260915 (user message and tool result, both 200).
|
|
475
487
|
"deepseek-v4.1-flash": ["text", "image"],
|
|
476
488
|
"glm-5.2": ["text"],
|
|
489
|
+
"glm-5.3": ["text"],
|
|
477
490
|
};
|
|
478
491
|
|
|
479
492
|
// 260721 Alibaba Token Plan International (ap-southeast-1 / Singapore, hardened 260721).
|
|
@@ -487,7 +500,7 @@ export const ALIBABA_INTL_TOKEN_PLAN_MODELS = [
|
|
|
487
500
|
"qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
|
|
488
501
|
"deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "deepseek-v3.2",
|
|
489
502
|
"kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5",
|
|
490
|
-
"glm-5.2", "glm-5.1", "glm-5",
|
|
503
|
+
"glm-5.2", "glm-5.3", "glm-5.1", "glm-5",
|
|
491
504
|
"MiniMax-M2.5",
|
|
492
505
|
];
|
|
493
506
|
export const ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS = [
|
|
@@ -589,7 +602,7 @@ export const ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
589
602
|
"deepseek-v4-pro": 1_000_000, "deepseek-v4-pro-0813": 1_000_000, "deepseek-v4-flash": 1_000_000,
|
|
590
603
|
"deepseek-v4-flash-0731": 1_000_000, "deepseek-v4.1-flash": 1_000_000, "deepseek-v3.2": 131_072,
|
|
591
604
|
"kimi-k2.7-code": 262_144, "kimi-k2.6": 262_144, "kimi-k2.5": 262_144,
|
|
592
|
-
"glm-5.2": 1_000_000, "glm-5.1": 202_752, "glm-5": 202_752,
|
|
605
|
+
"glm-5.2": 1_000_000, "glm-5.3": 1_000_000, "glm-5.1": 202_752, "glm-5": 202_752,
|
|
593
606
|
"MiniMax-M2.5": 196_608,
|
|
594
607
|
};
|
|
595
608
|
export const ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS: Record<string, number> = {
|
|
@@ -598,17 +611,17 @@ export const ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS: Record<string, number> = {
|
|
|
598
611
|
"deepseek-v4-pro": 393_216, "deepseek-v4-pro-0813": 393_216, "deepseek-v4-flash": 393_216,
|
|
599
612
|
"deepseek-v4-flash-0731": 393_216, "deepseek-v4.1-flash": 393_216, "deepseek-v3.2": 65_536,
|
|
600
613
|
"kimi-k2.7-code": 262_144, "kimi-k2.6": 262_144, "kimi-k2.5": 98_304,
|
|
601
|
-
"glm-5.2": 131_072, "glm-5.1": 128_000, "glm-5": 16_384,
|
|
614
|
+
"glm-5.2": 131_072, "glm-5.3": 131_072, "glm-5.1": 128_000, "glm-5": 16_384,
|
|
602
615
|
"MiniMax-M2.5": 32_768,
|
|
603
616
|
};
|
|
604
617
|
export const ALIBABA_TOKEN_PLAN_NO_VISION = [
|
|
605
618
|
"qwen3.7-max", "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash",
|
|
606
|
-
"deepseek-v4-flash-0731", "deepseek-v3.2", "glm-5.2", "glm-5.1", "glm-5", "MiniMax-M2.5",
|
|
619
|
+
"deepseek-v4-flash-0731", "deepseek-v3.2", "glm-5.2", "glm-5.3", "glm-5.1", "glm-5", "MiniMax-M2.5",
|
|
607
620
|
];
|
|
608
621
|
export const ALIBABA_TOKEN_PLAN_PRESERVE_REASONING = [
|
|
609
622
|
"qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
|
|
610
623
|
"deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash", "deepseek-v4-flash-0731",
|
|
611
|
-
"deepseek-v4.1-flash", "glm-5.2",
|
|
624
|
+
"deepseek-v4.1-flash", "glm-5.2", "glm-5.3",
|
|
612
625
|
];
|
|
613
626
|
|
|
614
627
|
// 260717 Kimi K3: the subscription endpoint uses one upstream id (`k3`) for both
|
|
@@ -960,3 +973,47 @@ export const CLINE_PASS_TEXT_ONLY_MODELS = CLINE_PASS_MODALITY_KNOWN_MODELS.filt
|
|
|
960
973
|
export const CLINE_PASS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
|
|
961
974
|
CLINE_PASS_MODALITY_KNOWN_MODELS.map(id => [id, CLINE_PASS_IMAGE_MODELS.has(id) ? ["text", "image"] : ["text"]]),
|
|
962
975
|
);
|
|
976
|
+
|
|
977
|
+
// Opper seed: bare *pool* names. A pool is every provider Opper serves that model through; Opper
|
|
978
|
+
// picks the route per request. Each name is the `.model` of a `pooled: true` entry in the public
|
|
979
|
+
// catalogue snapshot supplied by the original provider author
|
|
980
|
+
// (https://api.opper.ai/v3/models?limit=2000, captured 2026-09-14); `vendor/model` ids
|
|
981
|
+
// (anthropic/claude-sonnet-4-6) pin one route and stay valid, they are just not seeded.
|
|
982
|
+
export const OPPER_MODELS = [
|
|
983
|
+
"claude-sonnet-4-6",
|
|
984
|
+
"claude-opus-5",
|
|
985
|
+
"gpt-5.5",
|
|
986
|
+
"gpt-5.4-mini",
|
|
987
|
+
"gemini-3.8-flash",
|
|
988
|
+
"deepseek-v4-pro",
|
|
989
|
+
"kimi-k3",
|
|
990
|
+
"mistral-large-2512",
|
|
991
|
+
];
|
|
992
|
+
// Smallest value across each pool's members in that snapshot, capped at the lab model's own limit
|
|
993
|
+
// (kimi-k3 output); live discovery owns which models exist.
|
|
994
|
+
export const OPPER_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
995
|
+
"claude-sonnet-4-6": 1_000_000,
|
|
996
|
+
"claude-opus-5": 1_000_000,
|
|
997
|
+
"gpt-5.5": 1_050_000,
|
|
998
|
+
"gpt-5.4-mini": 400_000,
|
|
999
|
+
"gemini-3.8-flash": 1_048_576,
|
|
1000
|
+
"deepseek-v4-pro": 1_000_000,
|
|
1001
|
+
"kimi-k3": 1_048_576,
|
|
1002
|
+
"mistral-large-2512": 256_000,
|
|
1003
|
+
};
|
|
1004
|
+
export const OPPER_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = {
|
|
1005
|
+
"claude-sonnet-4-6": 64_000,
|
|
1006
|
+
"claude-opus-5": 128_000,
|
|
1007
|
+
"gpt-5.5": 128_000,
|
|
1008
|
+
"gpt-5.4-mini": 128_000,
|
|
1009
|
+
"gemini-3.8-flash": 65_536,
|
|
1010
|
+
"deepseek-v4-pro": 65_536,
|
|
1011
|
+
"kimi-k3": 131_072,
|
|
1012
|
+
"mistral-large-2512": 8_192,
|
|
1013
|
+
};
|
|
1014
|
+
// Pools whose members do not all accept image input (deepseek-v4-pro: no member does; kimi-k3: the
|
|
1015
|
+
// sference route is text-only), so the shared modality set is text.
|
|
1016
|
+
export const OPPER_TEXT_ONLY_MODELS = ["deepseek-v4-pro", "kimi-k3"];
|
|
1017
|
+
export const OPPER_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
|
|
1018
|
+
OPPER_MODELS.map(id => [id, OPPER_TEXT_ONLY_MODELS.includes(id) ? ["text"] : ["text", "image"]]),
|
|
1019
|
+
);
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Some routed models decorate the first and last lines as
|
|
4
4
|
// `*** Begin Patch ***` / `*** End Patch ***`. Codex rejects those otherwise
|
|
5
|
-
// valid custom-tool payloads. Repair is deliberately limited to
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// valid custom-tool payloads. Repair of executable bodies is deliberately limited to
|
|
6
|
+
// unambiguous wrapper mistakes: one recognized alternate field or one complete outer
|
|
7
|
+
// Markdown fence. Ordinary `exec` JavaScript remains byte-identical.
|
|
8
8
|
//
|
|
9
9
|
// This is the same intent boundary as `src/lib/tool-argument-integers.ts`:
|
|
10
10
|
// repair the one faithful reading, leave genuine patch content alone.
|
|
@@ -22,20 +22,52 @@ const PATCH_BEGIN = "*** Begin Patch";
|
|
|
22
22
|
const PATCH_END = "*** End Patch";
|
|
23
23
|
const TOP_LEVEL_PATCH_ENVELOPE = /^(\*\*\* Begin Patch(?: \*\*\*)?)(\r?\n)([\s\S]*)(\r?\n)(\*\*\* End Patch(?: \*\*\*)?)(\r?\n)?$/;
|
|
24
24
|
const PATCH_OPERATION_LINE = /^\*\*\* (?:Add|Update|Delete) File: .+$/m;
|
|
25
|
+
const OUTER_MARKDOWN_CODE_FENCE = /^```[^\r\n]*\r?\n([\s\S]*?)\r?\n```$/;
|
|
26
|
+
const FREEFORM_FALLBACK_KEYS: Readonly<Record<string, readonly string[]>> = {
|
|
27
|
+
exec: ["code", "script", "js", "javascript", "command", "cmd", "content"],
|
|
28
|
+
apply_patch: ["patch", "content"],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function stripMarkdownCodeFence(text: string, toolName: string): string {
|
|
32
|
+
if (toolName !== "exec" && toolName !== "apply_patch") return text;
|
|
33
|
+
const match = OUTER_MARKDOWN_CODE_FENCE.exec(text.trim());
|
|
34
|
+
return match ? match[1] : text;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The single-field wrappers `unwrapFreeformToolInput` accepts for one tool name, besides the
|
|
39
|
+
* canonical `input`.
|
|
40
|
+
*
|
|
41
|
+
* Exported so the streaming side can hold a buffer that is still turning into one of these.
|
|
42
|
+
* A second list of key names beside this one is how the streamed bytes and the completed item
|
|
43
|
+
* come to disagree, which is the defect it exists to prevent (#5047).
|
|
44
|
+
*/
|
|
45
|
+
export function freeformFallbackKeys(toolName: string): readonly string[] {
|
|
46
|
+
return FREEFORM_FALLBACK_KEYS[toolName] ?? [];
|
|
47
|
+
}
|
|
25
48
|
|
|
26
49
|
/** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
|
|
27
|
-
export function unwrapFreeformToolInput(argumentsText: unknown): string {
|
|
50
|
+
export function unwrapFreeformToolInput(argumentsText: unknown, toolName = ""): string {
|
|
28
51
|
if (typeof argumentsText !== "string") return "";
|
|
29
52
|
try {
|
|
30
53
|
const parsed: unknown = JSON.parse(argumentsText);
|
|
31
54
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
55
|
+
const record = parsed as Record<string, unknown>;
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(record, "input")) {
|
|
57
|
+
return typeof record.input === "string"
|
|
58
|
+
? stripMarkdownCodeFence(record.input, toolName)
|
|
59
|
+
: argumentsText;
|
|
60
|
+
}
|
|
61
|
+
const fallbackKeys = FREEFORM_FALLBACK_KEYS[toolName] ?? [];
|
|
62
|
+
const candidates = fallbackKeys.filter(key => typeof record[key] === "string");
|
|
63
|
+
if (candidates.length === 1) {
|
|
64
|
+
return stripMarkdownCodeFence(record[candidates[0]] as string, toolName);
|
|
65
|
+
}
|
|
34
66
|
}
|
|
35
67
|
} catch {
|
|
36
68
|
// The string is the freeform body, not nested JSON.
|
|
37
69
|
}
|
|
38
|
-
return argumentsText;
|
|
70
|
+
return stripMarkdownCodeFence(argumentsText, toolName);
|
|
39
71
|
}
|
|
40
72
|
|
|
41
73
|
/**
|
|
@@ -92,17 +124,18 @@ export function mayBecomePatchEnvelope(text: string): boolean {
|
|
|
92
124
|
/**
|
|
93
125
|
* Repair freeform input before Codex sees it.
|
|
94
126
|
*
|
|
95
|
-
* Only a bare or reserved-`functions`
|
|
96
|
-
* repair
|
|
97
|
-
* freeform input are unwrapped and left
|
|
127
|
+
* Only a bare or reserved-`functions` tool may receive fallback-field or outer-fence
|
|
128
|
+
* repair, and only `apply_patch` may receive delimiter repair. Remote namespaces own
|
|
129
|
+
* their grammar; those bodies and every other freeform input are unwrapped and left
|
|
130
|
+
* byte-exact.
|
|
98
131
|
*/
|
|
99
132
|
export function repairFreeformToolInput(
|
|
100
133
|
argumentsText: unknown,
|
|
101
134
|
toolName = "",
|
|
102
135
|
namespace?: string,
|
|
103
136
|
): string {
|
|
104
|
-
const unwrapped = unwrapFreeformToolInput(argumentsText);
|
|
105
137
|
const ownsApplyPatchGrammar = namespace === undefined || namespace === "functions";
|
|
138
|
+
const unwrapped = unwrapFreeformToolInput(argumentsText, ownsApplyPatchGrammar ? toolName : "");
|
|
106
139
|
return ownsApplyPatchGrammar && toolName === "apply_patch"
|
|
107
140
|
? normalizeApplyPatchDelimiters(unwrapped)
|
|
108
141
|
: unwrapped;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local memo pairing a bridged hosted `web_search` cell with the destination's own
|
|
3
|
+
* call and the result the proxy executed for it (issue #4587).
|
|
4
|
+
*
|
|
5
|
+
* The web-search passthrough bridge (`src/web-search/passthrough-bridge.ts`) intercepts the
|
|
6
|
+
* destination's `function_call` named `web_search`, runs the search itself, and shows the
|
|
7
|
+
* caller a hosted `web_search_call` cell under a proxy-minted `ws_<uuid>` id. The caller
|
|
8
|
+
* stores that cell and replays it on every later turn. The destination, which never produced a
|
|
9
|
+
* `web_search_call` in its life, then sees an unknown item type carrying a query and sources
|
|
10
|
+
* but no result — so it usually just searches again.
|
|
11
|
+
*
|
|
12
|
+
* This memo is what lets the pre-dispatch rewrite in the Responses adapter put the destination's
|
|
13
|
+
* own `function_call` and `function_call_output` back in that item's place. It records exactly
|
|
14
|
+
* what `appendBridgeSearchTurn` would have written onto a continuation leg, so a replayed turn
|
|
15
|
+
* and a continued turn show the destination the same conversation.
|
|
16
|
+
*
|
|
17
|
+
* Scope. Entries are keyed by the upstream destination in addition to the cell id. The cell id is
|
|
18
|
+
* a v4 UUID minted here, so it cannot collide across conversations, but an unscoped key would let
|
|
19
|
+
* a history replayed against a DIFFERENT provider resurrect a call that provider never made.
|
|
20
|
+
*
|
|
21
|
+
* Bounds and privacy. Result text is web content the caller already received, but it is still
|
|
22
|
+
* request-derived data: it lives in memory only, is never logged, serialized, or exported, and is
|
|
23
|
+
* bounded by entry count, total bytes, and TTL so a long-lived proxy cannot grow without limit.
|
|
24
|
+
*
|
|
25
|
+
* A miss is deliberately indistinguishable from "no entry": the caller leaves the replayed item
|
|
26
|
+
* alone. Neither re-running the search nor inventing a result is an acceptable recovery.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { reasoningReplayDestinationIdentity } from "./reasoning-replay-cache";
|
|
30
|
+
|
|
31
|
+
const MAX_ENTRIES = 64;
|
|
32
|
+
const MAX_TOTAL_BYTES = 512 * 1024;
|
|
33
|
+
const TTL_MS = 60 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
export interface BridgeSearchReplayEntry {
|
|
36
|
+
/** The destination's own call id, as it appeared on the intercepted item. */
|
|
37
|
+
callId: string;
|
|
38
|
+
/** The destination's own item id, replayed when the upstream supplied one. */
|
|
39
|
+
sourceItemId?: string;
|
|
40
|
+
/** The tool name the destination called, recorded rather than assumed. */
|
|
41
|
+
name: string;
|
|
42
|
+
/** The intercepted call's complete arguments text. */
|
|
43
|
+
argumentsText: string;
|
|
44
|
+
/** The tool result the bridge produced for that call. */
|
|
45
|
+
output: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface StoredEntry {
|
|
49
|
+
entry: BridgeSearchReplayEntry;
|
|
50
|
+
bytes: number;
|
|
51
|
+
at: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const entries = new Map<string, StoredEntry>();
|
|
55
|
+
let totalBytes = 0;
|
|
56
|
+
let clockForTests: (() => number) | null = null;
|
|
57
|
+
|
|
58
|
+
const now = (): number => clockForTests?.() ?? Date.now();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Identify the upstream destination a bridged search belongs to.
|
|
62
|
+
*
|
|
63
|
+
* Reuses the salted process-local destination digest the reasoning replay cache already defines,
|
|
64
|
+
* so both stores agree on what "the same upstream" means and neither invents a second notion of
|
|
65
|
+
* destination identity.
|
|
66
|
+
*/
|
|
67
|
+
export function bridgeSearchReplayScope(baseUrl: string | undefined): string | undefined {
|
|
68
|
+
return reasoningReplayDestinationIdentity(baseUrl);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function keyFor(scope: string, cellItemId: string): string {
|
|
72
|
+
return scope + "\u0000" + cellItemId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function drop(key: string, stored: StoredEntry): void {
|
|
76
|
+
entries.delete(key);
|
|
77
|
+
totalBytes -= stored.bytes;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sweep(at: number): void {
|
|
81
|
+
for (const [key, stored] of entries) {
|
|
82
|
+
if (at - stored.at >= TTL_MS) drop(key, stored);
|
|
83
|
+
}
|
|
84
|
+
// Map iteration is insertion-ordered, so the oldest surviving entry is always the first one.
|
|
85
|
+
while (entries.size > MAX_ENTRIES || totalBytes > MAX_TOTAL_BYTES) {
|
|
86
|
+
const oldest = entries.entries().next();
|
|
87
|
+
if (oldest.done) {
|
|
88
|
+
totalBytes = 0;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
drop(oldest.value[0], oldest.value[1]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Record one executed bridged search.
|
|
97
|
+
*
|
|
98
|
+
* An entry with no call id is not recorded: the restore would have to emit a `function_call`
|
|
99
|
+
* without one, which is not a valid item and could not be paired with its output anyway.
|
|
100
|
+
*/
|
|
101
|
+
export function rememberBridgeSearchReplay(
|
|
102
|
+
scope: string | undefined,
|
|
103
|
+
cellItemId: string,
|
|
104
|
+
entry: BridgeSearchReplayEntry,
|
|
105
|
+
): void {
|
|
106
|
+
if (!scope || cellItemId.length === 0 || entry.callId.length === 0) return;
|
|
107
|
+
const key = keyFor(scope, cellItemId);
|
|
108
|
+
const existing = entries.get(key);
|
|
109
|
+
if (existing) drop(key, existing);
|
|
110
|
+
const bytes = 2 * (
|
|
111
|
+
key.length
|
|
112
|
+
+ entry.callId.length
|
|
113
|
+
+ (entry.sourceItemId?.length ?? 0)
|
|
114
|
+
+ entry.name.length
|
|
115
|
+
+ entry.argumentsText.length
|
|
116
|
+
+ entry.output.length
|
|
117
|
+
);
|
|
118
|
+
// A single oversized result is refused outright rather than evicting the whole store for it.
|
|
119
|
+
if (bytes > MAX_TOTAL_BYTES) return;
|
|
120
|
+
entries.set(key, { entry: { ...entry }, bytes, at: now() });
|
|
121
|
+
totalBytes += bytes;
|
|
122
|
+
sweep(now());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Look up one recorded search without consuming it.
|
|
127
|
+
*
|
|
128
|
+
* The same cell is replayed on every subsequent turn of the conversation, so a consuming read
|
|
129
|
+
* would restore the pair once and then silently stop. Expiry stays absolute: a conversation that
|
|
130
|
+
* outlives the TTL degrades to today's behaviour (the hosted cell replays unchanged) rather than
|
|
131
|
+
* pinning entries open for as long as anyone keeps talking.
|
|
132
|
+
*/
|
|
133
|
+
export function peekBridgeSearchReplay(
|
|
134
|
+
scope: string | undefined,
|
|
135
|
+
cellItemId: string,
|
|
136
|
+
): BridgeSearchReplayEntry | undefined {
|
|
137
|
+
if (!scope || cellItemId.length === 0) return undefined;
|
|
138
|
+
const key = keyFor(scope, cellItemId);
|
|
139
|
+
const stored = entries.get(key);
|
|
140
|
+
if (!stored) return undefined;
|
|
141
|
+
if (now() - stored.at >= TTL_MS) {
|
|
142
|
+
drop(key, stored);
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
return stored.entry;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function clearBridgeSearchReplayCacheForTests(clock?: (() => number) | null): void {
|
|
149
|
+
entries.clear();
|
|
150
|
+
totalBytes = 0;
|
|
151
|
+
clockForTests = clock ?? null;
|
|
152
|
+
}
|
|
@@ -9,33 +9,43 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
9
9
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function unwrapPatchInput(value: string): string {
|
|
13
|
-
try {
|
|
14
|
-
const parsed: unknown = JSON.parse(value);
|
|
15
|
-
if (isPlainObject(parsed)) {
|
|
16
|
-
if (typeof parsed.input === "string") return parsed.input;
|
|
17
|
-
if (typeof parsed.patch === "string") return parsed.patch;
|
|
18
|
-
}
|
|
19
|
-
} catch {
|
|
20
|
-
// Native custom calls carry the patch body directly.
|
|
21
|
-
}
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
12
|
/**
|
|
26
13
|
* Convert a nested Code Mode helper call into unified-exec JavaScript.
|
|
27
14
|
*
|
|
28
15
|
* Parsed values are serialized as data, never interpolated as source, so command and patch text
|
|
29
16
|
* cannot escape the generated call. Invalid structured helper payloads are also passed as data so
|
|
30
17
|
* nested-tool validation can reject them without evaluating provider text as JavaScript.
|
|
18
|
+
*
|
|
19
|
+
* `wireToolName` is the name the body actually arrived under, which is not always the helper:
|
|
20
|
+
* a provider that got the NAME right and the BODY wrong sends `exec`, and the apply-patch
|
|
21
|
+
* helper is inferred from the payload. Recognition and compilation must read ONE canonical
|
|
22
|
+
* body, so both unwrap under that same name — see the apply-patch branch below. It defaults to
|
|
23
|
+
* the helper name, which is correct for the name-based path where the wire name IS the helper.
|
|
31
24
|
*/
|
|
32
|
-
export function compileCodeModeHelperInput(
|
|
25
|
+
export function compileCodeModeHelperInput(
|
|
26
|
+
argumentsText: unknown,
|
|
27
|
+
toolName: string,
|
|
28
|
+
wireToolName?: string,
|
|
29
|
+
): string {
|
|
33
30
|
if (typeof argumentsText !== "string") return "";
|
|
34
31
|
const helperName = toolName.startsWith("default.")
|
|
35
32
|
? toolName.slice("default.".length)
|
|
36
33
|
: toolName;
|
|
37
34
|
if (helperName === "apply_patch") {
|
|
38
|
-
|
|
35
|
+
// `resolveCodeModeHelperName` decides this IS an apply-patch call by reading
|
|
36
|
+
// `unwrapFreeformToolInput(argumentsText, wireToolName)`, which strips an outer Markdown
|
|
37
|
+
// fence and accepts that name's fallback fields (#4983). Compiling from a narrower unwrap
|
|
38
|
+
// meant a body accepted through a fence or a fallback field reached `tools.apply_patch`
|
|
39
|
+
// still wrapped, so the host rejected the JSON or the fence instead of applying the patch
|
|
40
|
+
// (#5046). One unwrap, one body, one decision.
|
|
41
|
+
//
|
|
42
|
+
// The vocabulary is the WIRE name rather than the helper name on purpose. `{"patch": ...}`
|
|
43
|
+
// is an apply_patch wrapper and is not an `exec` fallback field, and the recognizer already
|
|
44
|
+
// declines it under `exec`; reading it here would compile a body that recognition rejected,
|
|
45
|
+
// which is exactly the drift a second, looser unwrap introduces.
|
|
46
|
+
const patch = normalizeApplyPatchDelimiters(
|
|
47
|
+
unwrapFreeformToolInput(argumentsText, wireToolName ?? helperName),
|
|
48
|
+
);
|
|
39
49
|
return `const result = await tools.apply_patch(${JSON.stringify(patch)});\ntext(result);`;
|
|
40
50
|
}
|
|
41
51
|
let parsed: unknown = argumentsText;
|
|
@@ -106,5 +116,5 @@ export function resolveCodeModeHelperName(
|
|
|
106
116
|
// `tools.apply_patch(...)` JavaScript would be the mis-route this repair exists to avoid.
|
|
107
117
|
if (!declaresCodeModeExec(declaredNames)) return undefined;
|
|
108
118
|
if (typeof argumentsText !== "string" || argumentsText === "") return undefined;
|
|
109
|
-
return isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText)) ? "apply_patch" : undefined;
|
|
119
|
+
return isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText, "exec")) ? "apply_patch" : undefined;
|
|
110
120
|
}
|
|
@@ -326,7 +326,7 @@ export function restoreRoutedCustomCalls(
|
|
|
326
326
|
id: customToolItemId(item.id),
|
|
327
327
|
name: aliased ? targetName : item.name,
|
|
328
328
|
input: helper
|
|
329
|
-
? compileCodeModeHelperInput(sourceInput, helper)
|
|
329
|
+
? compileCodeModeHelperInput(sourceInput, helper, aliased ? String(item.name) : targetName)
|
|
330
330
|
: repairFreeformToolInput(
|
|
331
331
|
sourceInput,
|
|
332
332
|
targetName,
|
|
@@ -10,7 +10,90 @@ const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{
|
|
|
10
10
|
},
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Hosted-tool declaration names an operator may list in `unsupportedHostedTools`.
|
|
15
|
+
*
|
|
16
|
+
* A gateway must be able to deny anything a client can send it, so this is the set of
|
|
17
|
+
* nameless hosted/private declaration types the proxy recognizes on a Responses request.
|
|
18
|
+
* It is deliberately a closed vocabulary: the provider config schema ends in
|
|
19
|
+
* `.passthrough()`, so an unvalidated misspelling would be accepted, persisted, and then
|
|
20
|
+
* silently strip nothing -- which is the exact 400 the operator set the field to avoid
|
|
21
|
+
* (the `codexToolMode` lesson in #2106).
|
|
22
|
+
*/
|
|
23
|
+
export const DECLARABLE_HOSTED_TOOL_TYPES: ReadonlySet<string> = new Set([
|
|
24
|
+
"web_search",
|
|
25
|
+
"web_search_preview",
|
|
26
|
+
"file_search",
|
|
27
|
+
"computer_use_preview",
|
|
28
|
+
"computer_use",
|
|
29
|
+
"code_interpreter",
|
|
30
|
+
"image_generation",
|
|
31
|
+
"image_gen",
|
|
32
|
+
"mcp",
|
|
33
|
+
"tool_search",
|
|
34
|
+
"local_shell",
|
|
35
|
+
"x_search",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Spellings that name one capability. Declaring either member denies both, because the
|
|
40
|
+
* rest of the proxy already treats these as a single tool: the parser folds
|
|
41
|
+
* `web_search_preview` onto one name (`src/responses/parser-tools.ts`), Chat ingress
|
|
42
|
+
* accepts the pair together (`src/chat/inbound.ts`), and the canonical-field strip lists
|
|
43
|
+
* the pair in a single `toolTypes` set
|
|
44
|
+
* (`src/adapters/openai-responses/request-strips.ts`).
|
|
45
|
+
*
|
|
46
|
+
* Without the alias a capability declaration would be honoured for the spelling the
|
|
47
|
+
* operator happened to write and ignored for the one the client happened to send, which
|
|
48
|
+
* reproduces the original rejection while the config claims to have prevented it.
|
|
49
|
+
*/
|
|
50
|
+
const HOSTED_TOOL_ALIAS_GROUPS: ReadonlyArray<ReadonlySet<string>> = [
|
|
51
|
+
new Set(["web_search", "web_search_preview"]),
|
|
52
|
+
new Set(["image_generation", "image_gen"]),
|
|
53
|
+
new Set(["computer_use_preview", "computer_use"]),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const NO_DECLARED_HOSTED_TOOLS: ReadonlySet<string> = new Set();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Expand a provider's declared denials through the alias groups once per request, so the
|
|
60
|
+
* per-tool predicate stays a set lookup. Returns a shared empty set when the provider
|
|
61
|
+
* declares nothing, keeping the common path allocation-free.
|
|
62
|
+
*/
|
|
63
|
+
export function declaredUnsupportedHostedTools(
|
|
64
|
+
provider?: { unsupportedHostedTools?: readonly string[] },
|
|
65
|
+
): ReadonlySet<string> {
|
|
66
|
+
const declared = provider?.unsupportedHostedTools;
|
|
67
|
+
if (!Array.isArray(declared) || declared.length === 0) return NO_DECLARED_HOSTED_TOOLS;
|
|
68
|
+
const out = new Set<string>();
|
|
69
|
+
for (const raw of declared) {
|
|
70
|
+
if (typeof raw !== "string") continue;
|
|
71
|
+
const tool = raw.trim();
|
|
72
|
+
if (!tool) continue;
|
|
73
|
+
out.add(tool);
|
|
74
|
+
for (const group of HOSTED_TOOL_ALIAS_GROUPS) {
|
|
75
|
+
if (!group.has(tool)) continue;
|
|
76
|
+
for (const alias of group) out.add(alias);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out.size > 0 ? out : NO_DECLARED_HOSTED_TOOLS;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* True when forwarding this hosted tool to the model would be rejected upstream.
|
|
84
|
+
*
|
|
85
|
+
* `declaredUnsupported` is the provider's own capability declaration, expanded by
|
|
86
|
+
* `declaredUnsupportedHostedTools`. It is additive to the built-in table rather than a
|
|
87
|
+
* replacement for it: the table covers destinations that reject a tool regardless of how
|
|
88
|
+
* the operator configured them, so an operator who never heard of the field stays
|
|
89
|
+
* protected.
|
|
90
|
+
*/
|
|
91
|
+
export function isHostedToolUnsupportedForModel(
|
|
92
|
+
modelId: string,
|
|
93
|
+
tool: string,
|
|
94
|
+
baseUrl?: string,
|
|
95
|
+
declaredUnsupported?: ReadonlySet<string>,
|
|
96
|
+
): boolean {
|
|
97
|
+
if (declaredUnsupported?.has(tool)) return true;
|
|
15
98
|
return UNSUPPORTED_HOSTED_TOOLS.some(entry => entry.match(modelId, baseUrl) && entry.tools.has(tool));
|
|
16
99
|
}
|
package/src/responses/schema.ts
CHANGED
|
@@ -126,10 +126,17 @@ export const toolSchema = z.object({
|
|
|
126
126
|
|
|
127
127
|
const builtinToolSchema = z.object({ type: z.string() }).loose();
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Hosted tool types a client may declare on an inbound Responses request. Exported so the
|
|
131
|
+
* provider-side capability vocabulary in `src/responses/hosted-tool-policy.ts` can be
|
|
132
|
+
* asserted to cover all of them: a gateway must be able to deny anything it can be sent.
|
|
133
|
+
*/
|
|
134
|
+
export const HOSTED_TOOL_TYPES = [
|
|
130
135
|
"web_search", "web_search_preview", "file_search", "computer_use_preview",
|
|
131
136
|
"code_interpreter", "image_generation", "mcp",
|
|
132
|
-
]
|
|
137
|
+
] as const;
|
|
138
|
+
|
|
139
|
+
const hostedToolType = z.enum(HOSTED_TOOL_TYPES);
|
|
133
140
|
|
|
134
141
|
const allowedToolEntrySchema = z.object({ type: z.string(), name: z.string().optional() });
|
|
135
142
|
|