@iislee/opencodex 2.11.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/LICENSE +21 -0
- package/README.md +225 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +451 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-DTpMHS4F.js +67 -0
- package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/claude.svg +1 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/copilot.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/cursor.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/gemini.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/kiro.svg +14 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +1 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +102 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1003 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +72 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +127 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +211 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1214 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +329 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +576 -0
- package/src/adapters/cursor/protobuf-events.ts +563 -0
- package/src/adapters/cursor/protobuf-request.ts +714 -0
- package/src/adapters/cursor/request-builder.ts +255 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +505 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +52 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +303 -0
- package/src/adapters/google-antigravity-wire.ts +108 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +13 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +758 -0
- package/src/adapters/identity.ts +44 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +197 -0
- package/src/adapters/kiro-events.ts +179 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +96 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +215 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1898 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1005 -0
- package/src/adapters/openai-responses.ts +1137 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/bridge.ts +1619 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +765 -0
- package/src/claude/agents-inject.ts +243 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +381 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +509 -0
- package/src/claude/model-info.ts +151 -0
- package/src/claude/outbound.ts +872 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +268 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account-extended.ts +350 -0
- package/src/cli/account.ts +275 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +188 -0
- package/src/cli/claude.ts +286 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/doctor.ts +930 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +354 -0
- package/src/cli/index.ts +1113 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +142 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +336 -0
- package/src/cli/observe.ts +117 -0
- package/src/cli/opencode.ts +586 -0
- package/src/cli/pi.ts +188 -0
- package/src/cli/provider-runtime.ts +162 -0
- package/src/cli/provider.ts +463 -0
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +155 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/sync-cloud.ts +283 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/tray-proxy.ts +52 -0
- package/src/cli/v2.ts +173 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +377 -0
- package/src/clients/effective-status.ts +385 -0
- package/src/clients/probes/agy.ts +55 -0
- package/src/clients/probes/cc-switch.ts +110 -0
- package/src/clients/probes/claude.ts +90 -0
- package/src/clients/probes/codex.ts +125 -0
- package/src/clients/probes/grok.ts +29 -0
- package/src/clients/probes/opencode.ts +109 -0
- package/src/clients/probes/paseo.ts +55 -0
- package/src/clients/probes/pi.ts +55 -0
- package/src/cloud/onedrive-auth.ts +666 -0
- package/src/cloud/onedrive-graph.ts +108 -0
- package/src/cloud/settings.ts +75 -0
- package/src/cloud/sync.ts +212 -0
- package/src/cloud/types.ts +56 -0
- package/src/cloud/vault.ts +89 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +55 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +20 -0
- package/src/codex/app-server-processes.ts +756 -0
- package/src/codex/auth-api.ts +1540 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +352 -0
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +378 -0
- package/src/codex/catalog/bundled.ts +251 -0
- package/src/codex/catalog/effort.ts +355 -0
- package/src/codex/catalog/metadata.ts +180 -0
- package/src/codex/catalog/parsing.ts +456 -0
- package/src/codex/catalog/provider-fetch.ts +902 -0
- package/src/codex/catalog/sync.ts +620 -0
- package/src/codex/catalog.ts +12 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +969 -0
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/codex/history-provider.ts +776 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject.ts +799 -0
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +163 -0
- package/src/codex/main-account-cache.ts +32 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/model-cache.ts +227 -0
- package/src/codex/paths.ts +65 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +225 -0
- package/src/codex/project-config-warnings.ts +411 -0
- package/src/codex/quota.ts +411 -0
- package/src/codex/refresh.ts +53 -0
- package/src/codex/routing.ts +1477 -0
- package/src/codex/runtime.ts +538 -0
- package/src/codex/shim.ts +1189 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +469 -0
- package/src/codex/sync.ts +130 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +41 -0
- package/src/combos/request.ts +62 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +326 -0
- package/src/config.ts +2356 -0
- package/src/generated/jawcode-model-metadata.ts +104 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +545 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +103 -0
- package/src/grok/usage-hook/report.mjs +348 -0
- package/src/grok/usage-hook.ts +278 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +829 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +202 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +71 -0
- package/src/lib/bun-stream-caps.ts +126 -0
- package/src/lib/config-ownership.ts +360 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +165 -0
- package/src/lib/provider-outbound.ts +170 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +105 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +323 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/upstream-retry.ts +239 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +514 -0
- package/src/lib/winsw.ts +375 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/cursor.ts +211 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +399 -0
- package/src/oauth/index.ts +1174 -0
- package/src/oauth/key-providers.ts +108 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +577 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +630 -0
- package/src/oauth/token-guardian.ts +303 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/pi/extensions.ts +72 -0
- package/src/pi/home.ts +42 -0
- package/src/pi/index.ts +40 -0
- package/src/pi/models.ts +278 -0
- package/src/pi/packages.ts +219 -0
- package/src/pi/settings.ts +365 -0
- package/src/pi/status.ts +68 -0
- package/src/pi/sync.ts +75 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +205 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/context-cap.ts +65 -0
- package/src/providers/derive.ts +339 -0
- package/src/providers/free-directory.ts +184 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/key-failover.ts +203 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/openai-sidecar.ts +175 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/quota.ts +1260 -0
- package/src/providers/registry.ts +1600 -0
- package/src/providers/slug-codec.ts +67 -0
- package/src/providers/xai-transport.ts +141 -0
- package/src/reasoning-effort.ts +135 -0
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +656 -0
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +895 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +425 -0
- package/src/server/adapter-resolve.ts +80 -0
- package/src/server/auth-cors.ts +530 -0
- package/src/server/chat-completions.ts +368 -0
- package/src/server/claude-messages.ts +914 -0
- package/src/server/effort-policy.ts +172 -0
- package/src/server/gui-static.ts +123 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +476 -0
- package/src/server/index.ts +1126 -0
- package/src/server/lifecycle.ts +227 -0
- package/src/server/live.ts +598 -0
- package/src/server/management/agent-settings-routes.ts +1169 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/clients-routes.ts +63 -0
- package/src/server/management/cloud-sync-routes.ts +266 -0
- package/src/server/management/combo-routes.ts +220 -0
- package/src/server/management/config-routes.ts +422 -0
- package/src/server/management/context.ts +31 -0
- package/src/server/management/logs-usage-routes.ts +707 -0
- package/src/server/management/model-routes.ts +525 -0
- package/src/server/management/oauth-account-routes.ts +563 -0
- package/src/server/management/provider-routes.ts +556 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +90 -0
- package/src/server/management/system-restart.ts +179 -0
- package/src/server/management/system-routes.ts +117 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +215 -0
- package/src/server/management-auth.ts +267 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +116 -0
- package/src/server/proxy-liveness.ts +201 -0
- package/src/server/relay-eager.ts +313 -0
- package/src/server/relay.ts +1049 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1046 -0
- package/src/server/responses/collaboration.ts +354 -0
- package/src/server/responses/compact.ts +384 -0
- package/src/server/responses/core.ts +2758 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +224 -0
- package/src/server/responses.ts +9 -0
- package/src/server/search.ts +136 -0
- package/src/server/sse-payload-rewrite.ts +175 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +113 -0
- package/src/server/system-env.ts +413 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +471 -0
- package/src/service.ts +2554 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +290 -0
- package/src/tray/windows.ts +730 -0
- package/src/types.ts +1237 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +407 -0
- package/src/update/job.ts +1520 -0
- package/src/update/notify.ts +257 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +164 -0
- package/src/usage/log.ts +658 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +125 -0
- package/src/vision/index.ts +467 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +664 -0
- package/src/web-search/parse.ts +220 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-key 429 failover for non-OpenAI providers.
|
|
3
|
+
*
|
|
4
|
+
* When a provider's upstream returns 429, this module picks the next available key
|
|
5
|
+
* from `apiKeyPool`, puts the exhausted key into cooldown (respecting Retry-After),
|
|
6
|
+
* and returns a fresh provider config with the swapped key. If all keys are in
|
|
7
|
+
* cooldown, returns null so the caller surfaces the 429 to the client.
|
|
8
|
+
*
|
|
9
|
+
* Modelled after src/codex/routing.ts cooldown logic but scoped to plain API-key pools.
|
|
10
|
+
*/
|
|
11
|
+
import { saveConfigPreservingClaudeCode } from "../config";
|
|
12
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
13
|
+
import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
|
|
14
|
+
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
15
|
+
|
|
16
|
+
// ---- cooldown state (in-memory, same as codex/routing.ts) ----
|
|
17
|
+
|
|
18
|
+
interface KeyCooldown {
|
|
19
|
+
cooldownUntil: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
23
|
+
const MAX_COOLDOWN_MS = 10 * 60_000; // cap at 10 min for api-key rotation
|
|
24
|
+
|
|
25
|
+
/** Map<`${providerName}\0${keyId}`, KeyCooldown> */
|
|
26
|
+
const keyCooldowns = new Map<string, KeyCooldown>();
|
|
27
|
+
|
|
28
|
+
function cooldownKey(providerName: string, keyId: string): string {
|
|
29
|
+
return `${providerName}\0${keyId}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseRetryAfterMs(value: string | null | undefined, now = Date.now()): number | undefined {
|
|
33
|
+
const text = value?.trim();
|
|
34
|
+
if (!text) return undefined;
|
|
35
|
+
if (/^\d+(?:\.\d+)?$/.test(text)) {
|
|
36
|
+
const seconds = Number(text);
|
|
37
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
38
|
+
return Math.min(Math.max(Math.ceil(seconds * 1000), 1), MAX_COOLDOWN_MS);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const timestamp = Date.parse(text);
|
|
42
|
+
if (!Number.isFinite(timestamp)) return undefined;
|
|
43
|
+
const delay = timestamp - now;
|
|
44
|
+
return delay > 0 ? Math.min(delay, MAX_COOLDOWN_MS) : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isKeyInCooldown(providerName: string, keyId: string, now = Date.now()): boolean {
|
|
48
|
+
const entry = keyCooldowns.get(cooldownKey(providerName, keyId));
|
|
49
|
+
if (!entry) return false;
|
|
50
|
+
if (entry.cooldownUntil <= now) {
|
|
51
|
+
keyCooldowns.delete(cooldownKey(providerName, keyId));
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---- public API ----
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check whether a provider has multiple keys available for failover.
|
|
61
|
+
* Returns true only for key-auth providers with 2+ pool entries.
|
|
62
|
+
*/
|
|
63
|
+
export function hasKeyPoolFailover(provider: OcxProviderConfig): boolean {
|
|
64
|
+
if (provider.authMode === "oauth" || provider.authMode === "forward") return false;
|
|
65
|
+
return (provider.apiKeyPool?.length ?? 0) >= 2;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Record a 429 for the current key and attempt to switch to the next available one.
|
|
70
|
+
*
|
|
71
|
+
* @returns A new OcxProviderConfig with the swapped key (and mutated config on disk),
|
|
72
|
+
* or `null` when no alternative key is available (all in cooldown or pool < 2).
|
|
73
|
+
*
|
|
74
|
+
* The returned object is a snapshot of the PERSISTED config — it carries none of the
|
|
75
|
+
* registry backfills `routedProviderConfig` merges in at request time. Request paths must
|
|
76
|
+
* not assign it to an active route wholesale; use `rotateProviderTransportOn429`, which
|
|
77
|
+
* takes only the swapped key and keeps the routed provider intact.
|
|
78
|
+
*/
|
|
79
|
+
export function rotateKeyOn429(
|
|
80
|
+
config: OcxConfig,
|
|
81
|
+
providerName: string,
|
|
82
|
+
retryAfterHeader: string | null | undefined,
|
|
83
|
+
now = Date.now(),
|
|
84
|
+
attemptedKey?: string,
|
|
85
|
+
): OcxProviderConfig | null {
|
|
86
|
+
const provider = config.providers[providerName];
|
|
87
|
+
if (!provider) return null;
|
|
88
|
+
if (provider.authMode === "oauth" || provider.authMode === "forward") return null;
|
|
89
|
+
|
|
90
|
+
const pool = provider.apiKeyPool;
|
|
91
|
+
if (!pool || pool.length < 2) return null;
|
|
92
|
+
|
|
93
|
+
// Cool the key that ACTUALLY failed. Under concurrent 429s another request may already have
|
|
94
|
+
// rotated provider.apiKey — cooling the live key would punish an innocent replacement and can
|
|
95
|
+
// exhaust a 2-key pool from a single bad key. CAS semantics: callers pass the key they used.
|
|
96
|
+
const failedKey = attemptedKey ?? provider.apiKey;
|
|
97
|
+
const currentEntry = pool.find(e => e.key === failedKey);
|
|
98
|
+
if (currentEntry) {
|
|
99
|
+
const cooldownMs = parseRetryAfterMs(retryAfterHeader, now) ?? DEFAULT_COOLDOWN_MS;
|
|
100
|
+
keyCooldowns.set(cooldownKey(providerName, currentEntry.id), {
|
|
101
|
+
cooldownUntil: now + cooldownMs,
|
|
102
|
+
});
|
|
103
|
+
sweepExpiredOnWrite(now);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Lost the race: someone already rotated away from the failed key. If the live key is healthy,
|
|
107
|
+
// retry with it as-is instead of rotating a second time.
|
|
108
|
+
if (attemptedKey !== undefined && provider.apiKey !== attemptedKey) {
|
|
109
|
+
const liveEntry = pool.find(e => e.key === provider.apiKey);
|
|
110
|
+
if (liveEntry && !isKeyInCooldown(providerName, liveEntry.id, now)) {
|
|
111
|
+
return { ...provider };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Pick the next key that is NOT in cooldown
|
|
116
|
+
const currentIndex = currentEntry ? pool.indexOf(currentEntry) : -1;
|
|
117
|
+
for (let i = 1; i < pool.length; i++) {
|
|
118
|
+
const candidate = pool[(currentIndex + i) % pool.length]!;
|
|
119
|
+
if (!isKeyInCooldown(providerName, candidate.id, now)) {
|
|
120
|
+
// Swap active key
|
|
121
|
+
provider.apiKey = candidate.key;
|
|
122
|
+
saveConfigPreservingClaudeCode(config);
|
|
123
|
+
console.warn(
|
|
124
|
+
// Log ids only — labels are user-supplied free text and could carry secret material.
|
|
125
|
+
`[key-failover] ${providerName}: 429 on key ${currentEntry?.id ?? "?"}; rotating to key ${candidate.id}`,
|
|
126
|
+
);
|
|
127
|
+
return { ...provider };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// All keys in cooldown
|
|
132
|
+
console.warn(`[key-failover] ${providerName}: all ${pool.length} keys in cooldown; returning 429 to client`);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function sweepExpiredApiKeyCooldowns(now = Date.now()): number {
|
|
137
|
+
let removed = 0;
|
|
138
|
+
for (const [key, cooldown] of keyCooldowns) {
|
|
139
|
+
if (cooldown.cooldownUntil > now) continue;
|
|
140
|
+
keyCooldowns.delete(key);
|
|
141
|
+
removed += 1;
|
|
142
|
+
}
|
|
143
|
+
return removed;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface RotateProviderTransportOptions {
|
|
147
|
+
retryAfter?: string | null;
|
|
148
|
+
now?: number;
|
|
149
|
+
attemptedKey?: string;
|
|
150
|
+
promptCacheKey?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Rotate a failed key and re-apply provider-specific transport metadata to the replacement.
|
|
155
|
+
*
|
|
156
|
+
* `routedProvider` is the request's active provider (the `routedProviderConfig` output the
|
|
157
|
+
* route was built with). The result inherits it and swaps ONLY the API key: the persisted
|
|
158
|
+
* config that `rotateKeyOn429` snapshots predates registry backfill, so building the retry
|
|
159
|
+
* provider from that snapshot would silently drop every field the registry merged in at
|
|
160
|
+
* routing time (scalar flags like `promptCacheKey`/`parallelToolCalls`, merged model
|
|
161
|
+
* metadata such as `noTemperatureModels`, a pinned baseUrl). Mirrors the OAuth-401 replay
|
|
162
|
+
* path in src/server/responses/core.ts, which spreads `route.provider` for the same reason.
|
|
163
|
+
*/
|
|
164
|
+
export function rotateProviderTransportOn429(
|
|
165
|
+
config: OcxConfig,
|
|
166
|
+
providerName: string,
|
|
167
|
+
routedProvider: OcxProviderTransport,
|
|
168
|
+
options: RotateProviderTransportOptions = {},
|
|
169
|
+
): OcxProviderTransport | null {
|
|
170
|
+
const rotated = rotateKeyOn429(
|
|
171
|
+
config,
|
|
172
|
+
providerName,
|
|
173
|
+
options.retryAfter,
|
|
174
|
+
options.now,
|
|
175
|
+
options.attemptedKey,
|
|
176
|
+
);
|
|
177
|
+
return rotated
|
|
178
|
+
? resolveProviderTransport(
|
|
179
|
+
providerName,
|
|
180
|
+
{ ...routedProvider, apiKey: rotated.apiKey },
|
|
181
|
+
options.promptCacheKey,
|
|
182
|
+
)
|
|
183
|
+
: null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Clear cooldown state for a provider (e.g. after manual key management). */
|
|
187
|
+
export function clearKeyCooldowns(providerName?: string): void {
|
|
188
|
+
if (!providerName) {
|
|
189
|
+
keyCooldowns.clear();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const prefix = `${providerName}\0`;
|
|
193
|
+
for (const key of keyCooldowns.keys()) {
|
|
194
|
+
if (key.startsWith(prefix)) keyCooldowns.delete(key);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Visible-for-testing: get the cooldown-until timestamp for a key. */
|
|
199
|
+
export function getKeyCooldownUntil(providerName: string, keyId: string, now = Date.now()): number | null {
|
|
200
|
+
const entry = keyCooldowns.get(cooldownKey(providerName, keyId));
|
|
201
|
+
if (!entry) return null;
|
|
202
|
+
return entry.cooldownUntil > now ? entry.cooldownUntil : null;
|
|
203
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const KIRO_MODELS = [
|
|
2
|
+
"kiro-auto",
|
|
3
|
+
// OpenAI GPT-5.6 (Kiro experimental, us-east-1) — added to official catalog 2026-07-13
|
|
4
|
+
"gpt-5.6-sol",
|
|
5
|
+
"gpt-5.6-terra",
|
|
6
|
+
"gpt-5.6-luna",
|
|
7
|
+
"claude-sonnet-5",
|
|
8
|
+
"claude-opus-5",
|
|
9
|
+
"claude-opus-4.8",
|
|
10
|
+
"claude-opus-4.7",
|
|
11
|
+
"claude-opus-4.6",
|
|
12
|
+
"claude-opus-4.5",
|
|
13
|
+
"claude-sonnet-4.6",
|
|
14
|
+
"claude-sonnet-4.5",
|
|
15
|
+
"claude-sonnet-4.0",
|
|
16
|
+
"claude-haiku-4.5",
|
|
17
|
+
"deepseek-3.2",
|
|
18
|
+
"minimax-m2.5",
|
|
19
|
+
"minimax-m2.1",
|
|
20
|
+
"glm-5",
|
|
21
|
+
"qwen3-coder-next",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Per-model context windows as documented on Kiro's official model catalog
|
|
25
|
+
// (https://kiro.dev/docs/models/ — "Quick comparison", page updated 2026-07-14).
|
|
26
|
+
// "Auto" is a router with no fixed window on Kiro's table, so it is intentionally omitted.
|
|
27
|
+
export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
28
|
+
"gpt-5.6-sol": 272_000,
|
|
29
|
+
"gpt-5.6-terra": 272_000,
|
|
30
|
+
"gpt-5.6-luna": 272_000,
|
|
31
|
+
"claude-sonnet-5": 1_000_000,
|
|
32
|
+
"claude-opus-5": 1_000_000,
|
|
33
|
+
"claude-opus-4.8": 1_000_000,
|
|
34
|
+
"claude-opus-4.7": 1_000_000,
|
|
35
|
+
"claude-opus-4.6": 1_000_000,
|
|
36
|
+
"claude-opus-4.5": 200_000,
|
|
37
|
+
"claude-sonnet-4.6": 1_000_000,
|
|
38
|
+
"claude-sonnet-4.5": 200_000,
|
|
39
|
+
"claude-sonnet-4.0": 200_000,
|
|
40
|
+
"claude-haiku-4.5": 200_000,
|
|
41
|
+
"deepseek-3.2": 128_000,
|
|
42
|
+
"minimax-m2.5": 200_000,
|
|
43
|
+
"minimax-m2.1": 200_000,
|
|
44
|
+
"glm-5": 200_000,
|
|
45
|
+
"qwen3-coder-next": 256_000,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
49
|
+
|
|
50
|
+
// gpt-5.6-sol and claude-opus-5 send these values through Kiro's verified native effort fields
|
|
51
|
+
// (`reasoning.effort` and `output_config.effort` respectively). Other models map them to bounded
|
|
52
|
+
// thinking instructions until their native effort support is verified.
|
|
53
|
+
export const KIRO_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
|
|
54
|
+
KIRO_MODELS.map(id => [id, KIRO_REASONING_EFFORTS]),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export function normalizeKiroModelId(id: string): string {
|
|
58
|
+
let model = id.trim().toLowerCase();
|
|
59
|
+
model = model.replace(/^kiro\//, "").replace(/^kiro-/, "");
|
|
60
|
+
if (model === "auto") return "auto";
|
|
61
|
+
|
|
62
|
+
model = model.replace(/-\d{8}$/, "");
|
|
63
|
+
model = model.replace(/-(low|medium|high|xhigh|max)$/, "");
|
|
64
|
+
model = model.replace(/(\d+)-(\d+)/g, "$1.$2");
|
|
65
|
+
model = model.replace(/^claude-([\d.]+)-(sonnet|opus|haiku)$/, "claude-$2-$1");
|
|
66
|
+
return model;
|
|
67
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
|
|
2
|
+
|
|
3
|
+
function canonicalUsageProviderLabel(provider: string): string {
|
|
4
|
+
return provider === "chatgpt" || provider === "openai-multi" ? "openai" : provider;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function baseProviderLabel(provider: string): string {
|
|
8
|
+
const canonical = canonicalUsageProviderLabel(provider);
|
|
9
|
+
if (canonical !== provider) return canonical;
|
|
10
|
+
const cut = provider.lastIndexOf("-");
|
|
11
|
+
if (cut <= 0) return canonicalUsageProviderLabel(provider);
|
|
12
|
+
const suffix = provider.slice(cut + 1);
|
|
13
|
+
// `-main` is the legacy log label for the main Codex account (MAIN_CODEX_ACCOUNT_ID). New entries
|
|
14
|
+
// log under the base provider name, but historical `<provider>-main` entries must still collapse.
|
|
15
|
+
// ChatGPT auth-pool and OpenAI passthrough are the same Codex/OpenAI usage surface, so display
|
|
16
|
+
// summaries normalize them to one `openai` row after recognized main/pool suffixes are removed.
|
|
17
|
+
if (suffix === "main") return canonicalUsageProviderLabel(provider.slice(0, cut));
|
|
18
|
+
return CODEX_ACCOUNT_LOG_LABEL_RE.test(suffix) ? canonicalUsageProviderLabel(provider.slice(0, cut)) : provider;
|
|
19
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
getProviderRegistryEntry,
|
|
4
|
+
providerMatchesRegistryTransport,
|
|
5
|
+
type ProviderModelDiscoveryFilter,
|
|
6
|
+
type ProviderModelDiscoveryPredicate,
|
|
7
|
+
type ProviderModelDiscoveryScalar,
|
|
8
|
+
type ProviderModelDiscoverySpec,
|
|
9
|
+
} from "./registry";
|
|
10
|
+
|
|
11
|
+
/** Hard process-wide limits. Registry entries may lower, but never raise, these ceilings. */
|
|
12
|
+
export const MODEL_DISCOVERY_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
13
|
+
export const MODEL_DISCOVERY_MAX_MODELS = 2_000;
|
|
14
|
+
export const MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH = 1_024;
|
|
15
|
+
const MODEL_DISCOVERY_MAX_FILTER_VALUES = 256;
|
|
16
|
+
const MODEL_DISCOVERY_MAX_FILTER_STRING_LENGTH = 1_024;
|
|
17
|
+
const MODEL_DISCOVERY_MODEL_ID_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
18
|
+
|
|
19
|
+
export interface ResolvedProviderModelDiscovery {
|
|
20
|
+
spec?: ProviderModelDiscoverySpec;
|
|
21
|
+
maxResponseBytes: number;
|
|
22
|
+
maxModels: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ProviderModelsApiItem = Record<string, unknown> & { id: string };
|
|
26
|
+
|
|
27
|
+
export type ModelDiscoveryResponseFailure =
|
|
28
|
+
| "response_too_large"
|
|
29
|
+
| "invalid_json"
|
|
30
|
+
| "invalid_shape"
|
|
31
|
+
| "too_many_models";
|
|
32
|
+
|
|
33
|
+
export type BoundedDiscoveryJsonResult =
|
|
34
|
+
| { ok: true; value: unknown }
|
|
35
|
+
| { ok: false; reason: "response_too_large" | "invalid_json" };
|
|
36
|
+
|
|
37
|
+
export type ProviderModelItemsResult =
|
|
38
|
+
| { ok: true; items: ProviderModelsApiItem[]; rawCount: number }
|
|
39
|
+
| { ok: false; reason: "invalid_shape" | "too_many_models" };
|
|
40
|
+
|
|
41
|
+
export type ModelEnvelopeRowsResult =
|
|
42
|
+
| { ok: true; rows: unknown[] }
|
|
43
|
+
| { ok: false; reason: "invalid_shape" | "too_many_models" };
|
|
44
|
+
|
|
45
|
+
function positiveIntegerAtMost(value: number | undefined, hardLimit: number): number {
|
|
46
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return hardLimit;
|
|
47
|
+
return Math.min(Math.floor(value), hardLimit);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function discoveryPredicateError(predicate: ProviderModelDiscoveryPredicate): string | null {
|
|
51
|
+
if (!Array.isArray(predicate.path) || predicate.path.length === 0 || predicate.path.length > 8) {
|
|
52
|
+
return "predicate path must contain 1-8 segments";
|
|
53
|
+
}
|
|
54
|
+
if (predicate.path.some(segment => typeof segment !== "string" || !segment.trim() || segment.length > 64)) {
|
|
55
|
+
return "predicate path segments must be nonblank strings up to 64 characters";
|
|
56
|
+
}
|
|
57
|
+
const values = "equalsAny" in predicate
|
|
58
|
+
? predicate.equalsAny
|
|
59
|
+
: "containsAny" in predicate
|
|
60
|
+
? predicate.containsAny
|
|
61
|
+
: predicate.containsAll;
|
|
62
|
+
if (!Array.isArray(values) || values.length === 0 || values.length > 32) {
|
|
63
|
+
return "predicate values must contain 1-32 scalars";
|
|
64
|
+
}
|
|
65
|
+
if (values.some(value => (
|
|
66
|
+
(typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
67
|
+
|| (typeof value === "string" && (!value.trim() || value.length > 128))
|
|
68
|
+
|| (typeof value === "number" && !Number.isFinite(value))
|
|
69
|
+
))) {
|
|
70
|
+
return "predicate values must be finite booleans/numbers or nonblank strings up to 128 characters";
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Static registry validation used by parity tests; discovery metadata never comes from config. */
|
|
76
|
+
export function providerModelDiscoverySpecError(spec: ProviderModelDiscoverySpec): string | null {
|
|
77
|
+
if (spec.url && spec.path) return "url and path are mutually exclusive";
|
|
78
|
+
if (spec.url !== undefined) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = new URL(spec.url);
|
|
81
|
+
if (parsed.protocol !== "https:") return "absolute discovery url must use https";
|
|
82
|
+
if (parsed.username || parsed.password || parsed.hash) return "absolute discovery url must not contain credentials or a fragment";
|
|
83
|
+
} catch {
|
|
84
|
+
return "absolute discovery url must be valid";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (spec.path !== undefined) {
|
|
88
|
+
const path = spec.path.trim();
|
|
89
|
+
if (!path || path.length > 512) return "discovery path must be 1-512 characters";
|
|
90
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith("//") || path.includes("?") || path.includes("#")) {
|
|
91
|
+
return "discovery path must be a query-free relative/origin path";
|
|
92
|
+
}
|
|
93
|
+
if (path.includes("\\")) return "discovery path must use forward slashes";
|
|
94
|
+
if (path.split("/").some(segment => segment.replace(/%2e/gi, ".") === "..")) {
|
|
95
|
+
return "discovery path must not contain parent-directory segments";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const queryEntries = Object.entries(spec.query ?? {});
|
|
99
|
+
if (queryEntries.length > 32) return "discovery query may contain at most 32 entries";
|
|
100
|
+
if (queryEntries.some(([key, value]) => !key.trim() || key.length > 128 || typeof value !== "string" || value.length > 512)) {
|
|
101
|
+
return "discovery query keys/values exceed their bounds";
|
|
102
|
+
}
|
|
103
|
+
for (const [field, value, hardLimit] of [
|
|
104
|
+
["maxResponseBytes", spec.maxResponseBytes, MODEL_DISCOVERY_MAX_RESPONSE_BYTES],
|
|
105
|
+
["maxModels", spec.maxModels, MODEL_DISCOVERY_MAX_MODELS],
|
|
106
|
+
] as const) {
|
|
107
|
+
if (value !== undefined && (!Number.isInteger(value) || value <= 0 || value > hardLimit)) {
|
|
108
|
+
return `${field} must be a positive integer no greater than ${hardLimit}`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const [group, predicates] of Object.entries(spec.filter ?? {})) {
|
|
112
|
+
if (!Array.isArray(predicates) || predicates.length === 0 || predicates.length > 32) {
|
|
113
|
+
return `${group} must contain 1-32 predicates`;
|
|
114
|
+
}
|
|
115
|
+
for (const predicate of predicates) {
|
|
116
|
+
const error = discoveryPredicateError(predicate);
|
|
117
|
+
if (error) return `${group}: ${error}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function resolveProviderModelDiscovery(
|
|
124
|
+
providerName: string,
|
|
125
|
+
provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
|
|
126
|
+
): ResolvedProviderModelDiscovery {
|
|
127
|
+
const entry = providerMatchesRegistryTransport(providerName, provider)
|
|
128
|
+
? getProviderRegistryEntry(providerName)
|
|
129
|
+
: undefined;
|
|
130
|
+
const spec = entry?.modelDiscovery;
|
|
131
|
+
return {
|
|
132
|
+
...(spec ? { spec } : {}),
|
|
133
|
+
maxResponseBytes: positiveIntegerAtMost(spec?.maxResponseBytes, MODEL_DISCOVERY_MAX_RESPONSE_BYTES),
|
|
134
|
+
maxModels: positiveIntegerAtMost(spec?.maxModels, MODEL_DISCOVERY_MAX_MODELS),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function appendDiscoveryQuery(url: URL, query: Readonly<Record<string, string>> | undefined): URL {
|
|
139
|
+
for (const [key, value] of Object.entries(query ?? {})) url.searchParams.set(key, value);
|
|
140
|
+
return url;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Apply a registry-owned URL/path/query policy to the adapter's normal discovery endpoint. */
|
|
144
|
+
export function resolveProviderModelDiscoveryUrl(
|
|
145
|
+
providerName: string,
|
|
146
|
+
configuredProvider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
|
|
147
|
+
effectiveBaseUrl: string,
|
|
148
|
+
defaultUrl: string,
|
|
149
|
+
): string {
|
|
150
|
+
const { spec } = resolveProviderModelDiscovery(providerName, configuredProvider);
|
|
151
|
+
if (!spec) return defaultUrl;
|
|
152
|
+
|
|
153
|
+
let resolved: URL;
|
|
154
|
+
if (spec.url) {
|
|
155
|
+
resolved = new URL(spec.url);
|
|
156
|
+
} else if (spec.path) {
|
|
157
|
+
const base = new URL(effectiveBaseUrl.endsWith("/") ? effectiveBaseUrl : `${effectiveBaseUrl}/`);
|
|
158
|
+
resolved = spec.path.startsWith("/")
|
|
159
|
+
? new URL(spec.path, base.origin)
|
|
160
|
+
: new URL(spec.path, base);
|
|
161
|
+
} else {
|
|
162
|
+
resolved = new URL(defaultUrl);
|
|
163
|
+
}
|
|
164
|
+
return appendDiscoveryQuery(resolved, spec.query).toString();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function cancelWithoutWaiting(reader: ReadableStreamDefaultReader<Uint8Array>, reason: unknown): void {
|
|
168
|
+
try {
|
|
169
|
+
void reader.cancel(reason).catch(() => undefined);
|
|
170
|
+
} catch {
|
|
171
|
+
// A non-conforming stream may throw synchronously from cancel().
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Read a discovery response under a strict byte ceiling before JSON.parse can allocate freely. */
|
|
176
|
+
export async function readBoundedDiscoveryJson(
|
|
177
|
+
response: Response,
|
|
178
|
+
maxResponseBytes: number,
|
|
179
|
+
): Promise<BoundedDiscoveryJsonResult> {
|
|
180
|
+
const limit = positiveIntegerAtMost(maxResponseBytes, MODEL_DISCOVERY_MAX_RESPONSE_BYTES);
|
|
181
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
182
|
+
if (Number.isFinite(declaredLength) && declaredLength > limit) {
|
|
183
|
+
try {
|
|
184
|
+
void response.body?.cancel(new DOMException("Model discovery response is too large", "QuotaExceededError"))
|
|
185
|
+
.catch(() => undefined);
|
|
186
|
+
} catch {
|
|
187
|
+
// Best-effort cancellation only.
|
|
188
|
+
}
|
|
189
|
+
return { ok: false, reason: "response_too_large" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!response.body) return { ok: false, reason: "invalid_json" };
|
|
193
|
+
const reader = response.body.getReader();
|
|
194
|
+
const chunks: Uint8Array[] = [];
|
|
195
|
+
let total = 0;
|
|
196
|
+
try {
|
|
197
|
+
while (true) {
|
|
198
|
+
const { value, done } = await reader.read();
|
|
199
|
+
if (done) break;
|
|
200
|
+
if (!value || value.byteLength === 0) continue;
|
|
201
|
+
if (value.byteLength > limit - total) {
|
|
202
|
+
cancelWithoutWaiting(
|
|
203
|
+
reader,
|
|
204
|
+
new DOMException("Model discovery response is too large", "QuotaExceededError"),
|
|
205
|
+
);
|
|
206
|
+
return { ok: false, reason: "response_too_large" };
|
|
207
|
+
}
|
|
208
|
+
chunks.push(value);
|
|
209
|
+
total += value.byteLength;
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
try {
|
|
213
|
+
reader.releaseLock();
|
|
214
|
+
} catch {
|
|
215
|
+
// Cancellation may keep the lock briefly.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const bytes = new Uint8Array(total);
|
|
220
|
+
let offset = 0;
|
|
221
|
+
for (const chunk of chunks) {
|
|
222
|
+
bytes.set(chunk, offset);
|
|
223
|
+
offset += chunk.byteLength;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
227
|
+
return { ok: true, value: JSON.parse(text) as unknown };
|
|
228
|
+
} catch {
|
|
229
|
+
return { ok: false, reason: "invalid_json" };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function valueAtPath(item: Record<string, unknown>, path: readonly string[]): unknown {
|
|
234
|
+
let current: unknown = item;
|
|
235
|
+
for (const segment of path) {
|
|
236
|
+
if (current === null || typeof current !== "object" || Array.isArray(current)) return undefined;
|
|
237
|
+
current = (current as Record<string, unknown>)[segment];
|
|
238
|
+
}
|
|
239
|
+
return current;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function comparableScalar(value: unknown, caseInsensitive: boolean): ProviderModelDiscoveryScalar | undefined {
|
|
243
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return undefined;
|
|
244
|
+
if (typeof value === "string" && value.length > MODEL_DISCOVERY_MAX_FILTER_STRING_LENGTH) return undefined;
|
|
245
|
+
return caseInsensitive && typeof value === "string" ? value.toLowerCase() : value;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function comparableNeedles(
|
|
249
|
+
values: readonly ProviderModelDiscoveryScalar[],
|
|
250
|
+
caseInsensitive: boolean,
|
|
251
|
+
): ProviderModelDiscoveryScalar[] {
|
|
252
|
+
return values.map(value => caseInsensitive && typeof value === "string" ? value.toLowerCase() : value);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function predicateMatches(item: ProviderModelsApiItem, predicate: ProviderModelDiscoveryPredicate): boolean {
|
|
256
|
+
const caseInsensitive = predicate.caseInsensitive === true;
|
|
257
|
+
const raw = valueAtPath(item, predicate.path);
|
|
258
|
+
if ("equalsAny" in predicate) {
|
|
259
|
+
const value = comparableScalar(raw, caseInsensitive);
|
|
260
|
+
return value !== undefined && comparableNeedles(predicate.equalsAny, caseInsensitive).includes(value);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const collection = Array.isArray(raw);
|
|
264
|
+
const values: ProviderModelDiscoveryScalar[] = [];
|
|
265
|
+
if (collection) {
|
|
266
|
+
for (let i = 0; i < raw.length && i < MODEL_DISCOVERY_MAX_FILTER_VALUES; i += 1) {
|
|
267
|
+
const value = comparableScalar(raw[i], caseInsensitive);
|
|
268
|
+
if (value !== undefined) values.push(value);
|
|
269
|
+
}
|
|
270
|
+
} else if (typeof raw === "string") {
|
|
271
|
+
values.push(caseInsensitive ? raw.toLowerCase() : raw);
|
|
272
|
+
}
|
|
273
|
+
const needles = comparableNeedles(
|
|
274
|
+
"containsAny" in predicate ? predicate.containsAny : predicate.containsAll,
|
|
275
|
+
caseInsensitive,
|
|
276
|
+
);
|
|
277
|
+
if ("containsAny" in predicate) {
|
|
278
|
+
return needles.some(needle => values.some(value => (
|
|
279
|
+
!collection && typeof value === "string" && typeof needle === "string" ? value.includes(needle) : value === needle
|
|
280
|
+
)));
|
|
281
|
+
}
|
|
282
|
+
return needles.every(needle => values.some(value => (
|
|
283
|
+
!collection && typeof value === "string" && typeof needle === "string" ? value.includes(needle) : value === needle
|
|
284
|
+
)));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function providerModelMatchesDiscoveryFilter(
|
|
288
|
+
item: ProviderModelsApiItem,
|
|
289
|
+
filter: ProviderModelDiscoveryFilter | undefined,
|
|
290
|
+
): boolean {
|
|
291
|
+
if (!filter) return true;
|
|
292
|
+
if (filter.allOf && !filter.allOf.every(predicate => predicateMatches(item, predicate))) return false;
|
|
293
|
+
if (filter.anyOf && filter.anyOf.length > 0 && !filter.anyOf.some(predicate => predicateMatches(item, predicate))) return false;
|
|
294
|
+
if (filter.noneOf?.some(predicate => predicateMatches(item, predicate))) return false;
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Extract one allowlisted array envelope while enforcing the raw-row ceiling. */
|
|
299
|
+
export function extractModelEnvelopeRows(
|
|
300
|
+
value: unknown,
|
|
301
|
+
maxModels: number,
|
|
302
|
+
envelopeKeys: readonly string[],
|
|
303
|
+
): ModelEnvelopeRowsResult {
|
|
304
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
305
|
+
return { ok: false, reason: "invalid_shape" };
|
|
306
|
+
}
|
|
307
|
+
const record = value as Record<string, unknown>;
|
|
308
|
+
const rows = envelopeKeys.map(key => record[key]).find(Array.isArray);
|
|
309
|
+
if (!rows) return { ok: false, reason: "invalid_shape" };
|
|
310
|
+
const limit = positiveIntegerAtMost(maxModels, MODEL_DISCOVERY_MAX_MODELS);
|
|
311
|
+
if (rows.length > limit) return { ok: false, reason: "too_many_models" };
|
|
312
|
+
return { ok: true, rows };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Validate, bound, deduplicate, and declaratively filter OpenAI `{data:[...]}` or top-level arrays (Together `#617`). */
|
|
316
|
+
export function extractProviderModelItems(
|
|
317
|
+
value: unknown,
|
|
318
|
+
discovery: ResolvedProviderModelDiscovery,
|
|
319
|
+
): ProviderModelItemsResult {
|
|
320
|
+
const limit = positiveIntegerAtMost(discovery.maxModels, MODEL_DISCOVERY_MAX_MODELS);
|
|
321
|
+
let data: unknown[];
|
|
322
|
+
if (Array.isArray(value)) {
|
|
323
|
+
// Together-style top-level /models arrays. Catalog discovery must not treat a stray
|
|
324
|
+
// `models` key on openai-chat responses as valid — only `data` envelopes or top-level arrays.
|
|
325
|
+
if (value.length > limit) return { ok: false, reason: "too_many_models" };
|
|
326
|
+
data = value;
|
|
327
|
+
} else {
|
|
328
|
+
const envelope = extractModelEnvelopeRows(value, discovery.maxModels, ["data"]);
|
|
329
|
+
if (!envelope.ok) return envelope;
|
|
330
|
+
data = envelope.rows;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const items: ProviderModelsApiItem[] = [];
|
|
334
|
+
const seen = new Set<string>();
|
|
335
|
+
for (const raw of data) {
|
|
336
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
337
|
+
return { ok: false, reason: "invalid_shape" };
|
|
338
|
+
}
|
|
339
|
+
const id = (raw as { id?: unknown }).id;
|
|
340
|
+
if (typeof id !== "string") return { ok: false, reason: "invalid_shape" };
|
|
341
|
+
const normalizedId = id.trim();
|
|
342
|
+
if (
|
|
343
|
+
!normalizedId
|
|
344
|
+
|| normalizedId !== id
|
|
345
|
+
|| normalizedId.length > MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH
|
|
346
|
+
|| MODEL_DISCOVERY_MODEL_ID_CONTROL_CHARS.test(normalizedId)
|
|
347
|
+
) {
|
|
348
|
+
return { ok: false, reason: "invalid_shape" };
|
|
349
|
+
}
|
|
350
|
+
const item = raw as ProviderModelsApiItem;
|
|
351
|
+
if (!providerModelMatchesDiscoveryFilter(item, discovery.spec?.filter) || seen.has(normalizedId)) continue;
|
|
352
|
+
seen.add(normalizedId);
|
|
353
|
+
items.push(item);
|
|
354
|
+
}
|
|
355
|
+
return { ok: true, items, rawCount: data.length };
|
|
356
|
+
}
|