@iislee/opencodex 2.11.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +117 -22
- package/bin/ocx.mjs +245 -88
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +21 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +475 -62
- package/src/adapters/base.ts +82 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +637 -0
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +308 -0
- package/src/adapters/cursor/cursor-errors.ts +144 -6
- package/src/adapters/cursor/discovery.ts +122 -14
- package/src/adapters/cursor/effort-map.ts +106 -4
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +180 -59
- package/src/adapters/cursor/live-transport.ts +623 -170
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -3
- package/src/adapters/cursor/native-exec.ts +121 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +447 -75
- package/src/adapters/cursor/request-builder.ts +271 -35
- package/src/adapters/cursor/tool-definitions.ts +252 -12
- package/src/adapters/cursor/tool-result-normalize.ts +115 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +48 -1
- package/src/adapters/cursor.ts +336 -39
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +713 -41
- package/src/adapters/google-antigravity-wire.ts +48 -10
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +742 -86
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +65 -17
- package/src/adapters/kiro.ts +270 -73
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1377 -344
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1164 -64
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +105 -21
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +186 -0
- package/src/bridge.ts +512 -92
- package/src/chat/inbound.ts +57 -20
- package/src/chat/outbound.ts +97 -33
- package/src/claude/agents-inject.ts +40 -10
- package/src/claude/context-windows.ts +37 -14
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +88 -21
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +150 -19
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +648 -28
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +97 -19
- package/src/cli/agent.ts +53 -2
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +60 -15
- package/src/cli/claude.ts +129 -35
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +821 -0
- package/src/cli/doctor.ts +440 -56
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +34 -274
- package/src/cli/index.ts +417 -528
- package/src/cli/init.ts +5 -17
- package/src/cli/inspect.ts +230 -0
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +130 -2
- package/src/cli/models.ts +118 -16
- package/src/cli/observe.ts +123 -11
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +34 -3
- package/src/cli/provider.ts +25 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +476 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +57 -10
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +51 -4
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +137 -18
- package/src/cli/version-skew.ts +46 -0
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1442 -23
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +35 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +84 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +570 -107
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +893 -246
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +348 -32
- package/src/codex/autostart-health.ts +24 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +86 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +138 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +555 -45
- package/src/codex/catalog/native-models.ts +75 -0
- package/src/codex/catalog/parsing.ts +269 -30
- package/src/codex/catalog/provider-fetch.ts +1626 -144
- package/src/codex/catalog/sync.ts +1576 -187
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +386 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +676 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +436 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +30 -24
- package/src/codex/history-provider.ts +1016 -235
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +223 -0
- package/src/codex/inject-coordination.ts +373 -0
- package/src/codex/inject.ts +1114 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +81 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +524 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +360 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +622 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +675 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +1313 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +264 -21
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +514 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1065 -31
- package/src/codex/subagent-model-fallback.ts +437 -43
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +720 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +47 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +91 -9
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +1713 -709
- package/src/generated/compatibility-version.json +3324 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +157 -33
- package/src/images/plan.ts +24 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +155 -0
- package/src/integrations/registry.ts +166 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +433 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +715 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +167 -11
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +26 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +80 -5
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +6 -2
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +253 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +30 -4
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +462 -71
- package/src/oauth/key-providers.ts +25 -0
- package/src/oauth/kimi.ts +25 -3
- package/src/oauth/kiro.ts +95 -6
- package/src/oauth/local-token-detect.ts +13 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +133 -26
- package/src/oauth/token-guardian.ts +9 -3
- package/src/oauth/types.ts +15 -0
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +176 -0
- package/src/providers/context-cap.ts +26 -8
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +281 -6
- package/src/providers/fastwire.ts +507 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +35 -2
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +30 -0
- package/src/providers/quota.ts +1379 -40
- package/src/providers/registry.ts +1545 -112
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +94 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +32 -4
- package/src/reasoning-effort.ts +68 -3
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +384 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +435 -0
- package/src/responses/parser.ts +225 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +504 -0
- package/src/responses/schema.ts +12 -2
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +659 -31
- package/src/responses/thought-signature-replay.ts +364 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +405 -31
- package/src/routing/analytics.ts +379 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +155 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +289 -48
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +154 -72
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +445 -0
- package/src/server/claude-messages.ts +160 -44
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +1021 -190
- package/src/server/lifecycle.ts +336 -14
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +433 -116
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +64 -25
- package/src/server/management/config-routes.ts +511 -51
- package/src/server/management/context.ts +89 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +127 -52
- package/src/server/management/model-routes.ts +461 -133
- package/src/server/management/model-rows.ts +167 -0
- package/src/server/management/native-integration-routes.ts +770 -0
- package/src/server/management/oauth-account-routes.ts +90 -8
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +836 -159
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +289 -33
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +126 -23
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +161 -10
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +221 -54
- package/src/server/relay.ts +479 -87
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +298 -31
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +239 -36
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +444 -72
- package/src/server/responses/core.ts +3579 -436
- package/src/server/responses/empty-completion-guard.ts +333 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +89 -99
- package/src/server/responses/input-admission.ts +187 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +343 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +381 -0
- package/src/server/responses-custom-tool-repair.ts +363 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +363 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +376 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +319 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +53 -40
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1153 -94
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +911 -0
- package/src/types/provider.ts +614 -0
- package/src/types/request.ts +397 -0
- package/src/types/tools.ts +236 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +107 -1237
- package/src/update/index.ts +37 -23
- package/src/update/job.ts +445 -68
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +266 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +287 -16
- package/src/usage/log.ts +666 -50
- package/src/usage/summary.ts +388 -17
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +27 -16
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +119 -0
- package/src/web-search/executor.ts +21 -6
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +284 -54
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -20,11 +20,39 @@
|
|
|
20
20
|
* targeting it is the caller's explicit act.
|
|
21
21
|
*/
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
|
-
import {
|
|
23
|
+
import { existsSync } from "node:fs";
|
|
24
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
24
25
|
import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
26
|
+
import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
|
|
27
|
+
import { providerCodexAccountMode } from "../providers/registry";
|
|
28
|
+
import { sanitizeCodexReasoningEfforts } from "../reasoning-effort";
|
|
25
29
|
import { probeHostname } from "../server/proxy-liveness";
|
|
26
30
|
import type { OcxConfig } from "../types";
|
|
27
31
|
|
|
32
|
+
export type { ConfigFormat };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One entry opencodex owns inside a client's config: the JSON path to it and
|
|
36
|
+
* the value we put there.
|
|
37
|
+
*
|
|
38
|
+
* A path list rather than a single provider key because ownership is not
|
|
39
|
+
* always one entry — Kimi owns its provider block AND one model entry per
|
|
40
|
+
* model, and a writer that only knew about the provider would strand the rest
|
|
41
|
+
* (devlog 260802 006 §2).
|
|
42
|
+
*/
|
|
43
|
+
export interface ManagedFragment {
|
|
44
|
+
path: readonly string[];
|
|
45
|
+
value: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Everything opencodex contributes to one client's config, as one unit. */
|
|
49
|
+
export interface ManagedContribution {
|
|
50
|
+
clientId: ExportClientId;
|
|
51
|
+
fragments: readonly ManagedFragment[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type BuildContribution = (ctx: ExportContext) => ManagedContribution;
|
|
55
|
+
|
|
28
56
|
export interface OpencodeLaunchEnv {
|
|
29
57
|
[key: string]: string | undefined;
|
|
30
58
|
}
|
|
@@ -81,12 +109,42 @@ export const OPENCODE_API_KEY_ENV = "OPENCODEX_OPENCODE_API_KEY";
|
|
|
81
109
|
/** Env reference shared by apiKey and the dedicated proxy admission header. */
|
|
82
110
|
export const OPENCODE_API_KEY_ENV_REF = `{env:${OPENCODE_API_KEY_ENV}}`;
|
|
83
111
|
|
|
84
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Hermes interpolates `${VAR}` anywhere in config.yaml, so the credential stays
|
|
114
|
+
* in the environment exactly as it does for OpenCode.
|
|
115
|
+
*/
|
|
116
|
+
export const HERMES_API_KEY_ENV = "OPENCODEX_HERMES_API_KEY";
|
|
117
|
+
export const HERMES_API_KEY_ENV_REF = `\${${HERMES_API_KEY_ENV}}`;
|
|
118
|
+
|
|
119
|
+
/** OpenClaw interpolates `${UPPERCASE_VAR}` and fails closed when it is unset. */
|
|
120
|
+
export const OPENCLAW_API_KEY_ENV = "OPENCODEX_OPENCLAW_API_KEY";
|
|
121
|
+
export const OPENCLAW_API_KEY_ENV_REF = `\${${OPENCLAW_API_KEY_ENV}}`;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Placeholder credential for loopback-only clients (Kimi, Pi). A loopback
|
|
125
|
+
* bind needs no real admission key, so we emit the same placeholder the Grok
|
|
126
|
+
* managed block uses rather than a user secret. Pi resolves `apiKey` before
|
|
127
|
+
* building its model list and hides the provider when an env reference is unset.
|
|
128
|
+
*/
|
|
129
|
+
export const LOOPBACK_API_KEY_PLACEHOLDER = "opencodex-loopback";
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Legacy Pi launch env var. Newer exports use {@link LOOPBACK_API_KEY_PLACEHOLDER}
|
|
133
|
+
* in models.json; `ocx pi` still seeds this env so older `$OPENCODEX_API_KEY`
|
|
134
|
+
* configs keep working.
|
|
135
|
+
*/
|
|
85
136
|
export const PI_API_KEY_ENV = "OPENCODEX_API_KEY";
|
|
86
137
|
|
|
87
|
-
/** Pi
|
|
138
|
+
/** Pi env-reference form for older managed blocks (`$OPENCODEX_API_KEY`). */
|
|
88
139
|
export const PI_API_KEY_ENV_REF = `$${PI_API_KEY_ENV}`;
|
|
89
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Gajae's `apiKeyEnv` is env-name-only and fail-closed. Its sibling `apiKey`
|
|
143
|
+
* falls back to treating the literal text as the token when the variable is
|
|
144
|
+
* unset, which would silently ship a bogus credential — so we never emit it.
|
|
145
|
+
*/
|
|
146
|
+
export const GAJAE_API_KEY_ENV = "OPENCODEX_GAJAE_API_KEY";
|
|
147
|
+
|
|
90
148
|
/** Pi's wire-dialect selector for an OpenAI-compatible endpoint. */
|
|
91
149
|
const PI_API_DIALECT = "openai-completions";
|
|
92
150
|
|
|
@@ -124,11 +182,331 @@ export function opencodeGlobalConfigPath(
|
|
|
124
182
|
return join(xdg, "opencode", "opencode.json");
|
|
125
183
|
}
|
|
126
184
|
|
|
185
|
+
const OMP_PROFILE_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
186
|
+
const OMP_WINDOWS_RESERVED_PROFILE_RE = /^(?:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?$/i;
|
|
187
|
+
|
|
188
|
+
function ompProfileName(env: OpencodeLaunchEnv): string | undefined {
|
|
189
|
+
// OMP_PROFILE wins by presence, even when explicitly empty; PI_PROFILE is
|
|
190
|
+
// only the legacy fallback when the canonical variable is undefined.
|
|
191
|
+
const raw = env.OMP_PROFILE !== undefined ? env.OMP_PROFILE : env.PI_PROFILE;
|
|
192
|
+
const profile = raw?.trim();
|
|
193
|
+
if (!profile || profile === "default") return undefined;
|
|
194
|
+
if (
|
|
195
|
+
profile === "."
|
|
196
|
+
|| profile === ".."
|
|
197
|
+
|| profile.endsWith(".")
|
|
198
|
+
|| !OMP_PROFILE_NAME_RE.test(profile)
|
|
199
|
+
|| OMP_WINDOWS_RESERVED_PROFILE_RE.test(profile)
|
|
200
|
+
) {
|
|
201
|
+
throw new ClientPathError(`Invalid OMP profile "${raw}"`);
|
|
202
|
+
}
|
|
203
|
+
return profile;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Pi resolves its agent directory from `PI_CODING_AGENT_DIR`, falling back to
|
|
208
|
+
* `~/.pi/agent`. `ompAgentDir` below already reads that variable — OMP is a Pi
|
|
209
|
+
* derivative — so Pi's own resolver honoring it is what makes the two agree
|
|
210
|
+
* rather than a new claim about Pi's contract.
|
|
211
|
+
*
|
|
212
|
+
* A relative override is refused for the same reason MCode's and ZCode's are: a
|
|
213
|
+
* background proxy and a foreground client can have different working
|
|
214
|
+
* directories, and would otherwise disagree about which file is named.
|
|
215
|
+
*/
|
|
216
|
+
export function piAgentDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
217
|
+
const override = env.PI_CODING_AGENT_DIR?.trim();
|
|
218
|
+
if (override) return absoluteClientPath(override, home, "PI_CODING_AGENT_DIR");
|
|
219
|
+
return join(home, ".pi", "agent");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Pi's canonical custom-provider catalog. */
|
|
223
|
+
export function piConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
224
|
+
return join(piAgentDir(env, home), "models.json");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Resolve the global Oh My Pi agent directory using OMP's own env precedence. */
|
|
228
|
+
export function ompAgentDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
229
|
+
const profile = ompProfileName(env);
|
|
230
|
+
if (!profile) {
|
|
231
|
+
const override = env.PI_CODING_AGENT_DIR?.trim();
|
|
232
|
+
if (override) return absoluteClientPath(override, home, "PI_CODING_AGENT_DIR");
|
|
233
|
+
}
|
|
234
|
+
// OMP treats PI_CONFIG_DIR as a directory name relative to the user's home,
|
|
235
|
+
// even when the value starts with `/` or `~`. Mirror that path.join contract
|
|
236
|
+
// exactly instead of assigning those values different filesystem semantics.
|
|
237
|
+
const root = join(home, env.PI_CONFIG_DIR || ".omp");
|
|
238
|
+
return profile ? join(root, "profiles", profile, "agent") : join(root, "agent");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** OMP's canonical custom-provider catalog. */
|
|
242
|
+
export function ompModelsConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
243
|
+
const agentDir = ompAgentDir(env, home);
|
|
244
|
+
const yamlFallback = join(agentDir, "models.yaml");
|
|
245
|
+
const canonical = join(agentDir, "models.yml");
|
|
246
|
+
return !existsSync(canonical) && existsSync(yamlFallback) ? yamlFallback : canonical;
|
|
247
|
+
}
|
|
248
|
+
|
|
127
249
|
/** Compose the OpenAI-compatible proxy base URL from a live probe result. */
|
|
128
250
|
export function opencodeProxyBaseUrl(port: number, hostname?: string): string {
|
|
129
251
|
return `http://${probeHostname(hostname)}:${port}/v1`;
|
|
130
252
|
}
|
|
131
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Hermes resolves its home the way cc-switch's writer does: an explicit
|
|
256
|
+
* `HERMES_HOME`, then Windows `%LOCALAPPDATA%\hermes`, then `~/.hermes`.
|
|
257
|
+
*/
|
|
258
|
+
export function hermesHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
259
|
+
const override = env.HERMES_HOME?.trim();
|
|
260
|
+
if (override) return override;
|
|
261
|
+
if (process.platform === "win32") {
|
|
262
|
+
const local = env.LOCALAPPDATA?.trim();
|
|
263
|
+
return join(local && local.length > 0 ? local : join(home, "AppData", "Local"), "hermes");
|
|
264
|
+
}
|
|
265
|
+
return join(home, ".hermes");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function hermesConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
269
|
+
return join(hermesHomeDir(env, home), "config.yaml");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Expand `~` against the effective home; REFUSE anything still relative.
|
|
274
|
+
*
|
|
275
|
+
* A first attempt at this called `resolve()` and claimed the path would "mean
|
|
276
|
+
* the same thing next time". It does not: `resolve()` only anchors the current
|
|
277
|
+
* invocation, so applying from one directory and disabling from another still
|
|
278
|
+
* resolved two different files — the second reported "not applied" and left
|
|
279
|
+
* the managed block behind with nothing claiming it.
|
|
280
|
+
*
|
|
281
|
+
* There is no honest way to recover the other process's cwd, so a relative
|
|
282
|
+
* selector is refused at the boundary instead of being silently anchored to
|
|
283
|
+
* whichever directory we happened to start in. A `~` path IS stable, because
|
|
284
|
+
* it anchors to the effective home rather than the cwd.
|
|
285
|
+
*/
|
|
286
|
+
export class ClientPathError extends Error {}
|
|
287
|
+
|
|
288
|
+
function absoluteClientPath(raw: string, home: string, variable: string): string {
|
|
289
|
+
const trimmed = raw.trim();
|
|
290
|
+
if (trimmed === "~") return home;
|
|
291
|
+
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) return join(home, trimmed.slice(2));
|
|
292
|
+
if (!isAbsolute(trimmed)) {
|
|
293
|
+
throw new ClientPathError(
|
|
294
|
+
`${variable} must be an absolute path or start with ~; "${trimmed}" depends on the working directory, `
|
|
295
|
+
+ "so opencodex and the client would disagree about which file it names.",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return trimmed;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* OpenClaw's EFFECTIVE home: `OPENCLAW_HOME` outranks the OS home.
|
|
303
|
+
*
|
|
304
|
+
* Everything below derives from this, which is why it is separate: a profile
|
|
305
|
+
* directory and the default state directory both hang off the effective home,
|
|
306
|
+
* not off `homedir()`.
|
|
307
|
+
*/
|
|
308
|
+
function openclawEffectiveHome(env: OpencodeLaunchEnv, home: string): string {
|
|
309
|
+
const override = env.OPENCLAW_HOME?.trim();
|
|
310
|
+
return override ? absoluteClientPath(override, home, "OPENCLAW_HOME") : home;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* OpenClaw's state directory, in the gateway's own precedence order:
|
|
315
|
+
*
|
|
316
|
+
* 1. `OPENCLAW_STATE_DIR` — an explicit relocation wins outright.
|
|
317
|
+
* 2. `OPENCLAW_PROFILE` — a named profile is `.openclaw-<profile>` under the
|
|
318
|
+
* effective home. `default` is the unnamed profile, so it stays
|
|
319
|
+
* `.openclaw`.
|
|
320
|
+
* 3. `.openclaw` under the effective home (`OPENCLAW_HOME` or the OS home).
|
|
321
|
+
*
|
|
322
|
+
* This is also what "is it installed?" detection looks at, so it has to follow
|
|
323
|
+
* the same selectors the gateway does — otherwise an operator running a
|
|
324
|
+
* profile reads as not installed while their gateway runs fine. We honor the
|
|
325
|
+
* ENVIRONMENT selectors only: a `--profile` flag passed to some other process
|
|
326
|
+
* is not something we can observe, and guessing it would be worse than
|
|
327
|
+
* following the same environment the user gave us.
|
|
328
|
+
*/
|
|
329
|
+
export function openclawHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
330
|
+
const stateDir = env.OPENCLAW_STATE_DIR?.trim();
|
|
331
|
+
const effectiveHome = openclawEffectiveHome(env, home);
|
|
332
|
+
if (stateDir) return absoluteClientPath(stateDir, effectiveHome, "OPENCLAW_STATE_DIR");
|
|
333
|
+
const profile = env.OPENCLAW_PROFILE?.trim();
|
|
334
|
+
// OpenClaw compares the profile name case-insensitively, so `DEFAULT` is
|
|
335
|
+
// still the unnamed profile rather than a `.openclaw-DEFAULT` directory.
|
|
336
|
+
if (profile && profile.toLowerCase() !== "default") return join(effectiveHome, `.openclaw-${profile}`);
|
|
337
|
+
/*
|
|
338
|
+
* `.clawdbot` is not migration debris — OpenClaw still treats it as an
|
|
339
|
+
* active runtime candidate, preferring the modern directory when it exists
|
|
340
|
+
* and otherwise selecting the legacy one. Mirroring that order is not
|
|
341
|
+
* guessing: an install that has not migrated is one this integration would
|
|
342
|
+
* otherwise report as "not installed" while its gateway runs fine.
|
|
343
|
+
*/
|
|
344
|
+
const modern = join(effectiveHome, ".openclaw");
|
|
345
|
+
if (existsSync(modern)) return modern;
|
|
346
|
+
const legacy = join(effectiveHome, ".clawdbot");
|
|
347
|
+
if (existsSync(legacy)) return legacy;
|
|
348
|
+
return modern;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The config file OpenClaw actually reads.
|
|
353
|
+
*
|
|
354
|
+
* An explicit `OPENCLAW_CONFIG_PATH` wins outright — it is a file selector, so
|
|
355
|
+
* it does NOT relocate the state directory that detection looks at. Otherwise
|
|
356
|
+
* `openclaw.json` under the resolved state directory.
|
|
357
|
+
*
|
|
358
|
+
* Ignoring these selectors meant the toggle could report success after writing
|
|
359
|
+
* `~/.openclaw/openclaw.json` while the running gateway read somewhere else —
|
|
360
|
+
* and snapshot the wrong file, so the rollback promise pointed at a file
|
|
361
|
+
* nobody loads.
|
|
362
|
+
*/
|
|
363
|
+
export function openclawConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
364
|
+
const explicit = env.OPENCLAW_CONFIG_PATH?.trim();
|
|
365
|
+
if (explicit) return absoluteClientPath(explicit, openclawEffectiveHome(env, home), "OPENCLAW_CONFIG_PATH");
|
|
366
|
+
/*
|
|
367
|
+
* OpenClaw searches FILE candidates, not directories: a `.openclaw`
|
|
368
|
+
* directory that exists but holds no config does not beat an actual
|
|
369
|
+
* `.clawdbot/clawdbot.json`. Checking the directory first picked an
|
|
370
|
+
* absent modern file over a real legacy one and wrote where nothing reads.
|
|
371
|
+
*
|
|
372
|
+
* An explicit state dir still scopes the search to that directory, because
|
|
373
|
+
* the operator named it.
|
|
374
|
+
*/
|
|
375
|
+
const stateOverride = env.OPENCLAW_STATE_DIR?.trim();
|
|
376
|
+
const effectiveHome = openclawEffectiveHome(env, home);
|
|
377
|
+
const profile = env.OPENCLAW_PROFILE?.trim();
|
|
378
|
+
const scoped = stateOverride !== undefined && stateOverride !== ""
|
|
379
|
+
|| (profile !== undefined && profile !== "" && profile.toLowerCase() !== "default");
|
|
380
|
+
const stateDir = openclawHomeDir(env, home);
|
|
381
|
+
const candidates = scoped
|
|
382
|
+
? [join(stateDir, "openclaw.json"), join(stateDir, "clawdbot.json")]
|
|
383
|
+
: [
|
|
384
|
+
join(effectiveHome, ".openclaw", "openclaw.json"),
|
|
385
|
+
join(effectiveHome, ".openclaw", "clawdbot.json"),
|
|
386
|
+
join(effectiveHome, ".clawdbot", "openclaw.json"),
|
|
387
|
+
join(effectiveHome, ".clawdbot", "clawdbot.json"),
|
|
388
|
+
];
|
|
389
|
+
for (const candidate of candidates) {
|
|
390
|
+
if (existsSync(candidate)) return candidate;
|
|
391
|
+
}
|
|
392
|
+
return candidates[0]!;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function kimiHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
396
|
+
const override = env.KIMI_CODE_HOME?.trim();
|
|
397
|
+
return override && override.length > 0 ? override : join(home, ".kimi-code");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function kimiConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
401
|
+
return join(kimiHomeDir(env, home), "config.toml");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function gajaeHomeDir(_env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
405
|
+
return join(home, ".gjc");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function gajaeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
409
|
+
return join(gajaeHomeDir(env, home), "agent", "models.yml");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** DSH_HOME uses the raw nonblank value; trimming it would name a different path. */
|
|
413
|
+
export function dshHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
414
|
+
const raw = env.DSH_HOME;
|
|
415
|
+
if (raw === undefined || raw.trim().length === 0) return join(home, ".dsh");
|
|
416
|
+
if (raw === "~") return home;
|
|
417
|
+
if (raw.startsWith("~/") || raw.startsWith("~\\")) return join(home, raw.slice(2));
|
|
418
|
+
if (!isAbsolute(raw)) {
|
|
419
|
+
throw new ClientPathError(
|
|
420
|
+
`DSH_HOME must be an absolute path or start with ~; "${raw}" depends on the working directory, `
|
|
421
|
+
+ "so opencodex and DSH would disagree about which settings file it names.",
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
// DSH calls node:path.resolve after tilde expansion. Preserve the raw value
|
|
425
|
+
// for the decision above, then normalize the absolute spelling the same way
|
|
426
|
+
// so both processes bind ownership and locks to one path string.
|
|
427
|
+
return resolve(raw);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function dshConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
431
|
+
return join(dshHomeDir(env, home), "settings.yaml");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* MiniMax Code stores runtime state under `MINIMAX_DATA_DIR`, then the legacy
|
|
436
|
+
* `MAVIS_DATA_DIR`, and finally `~/.minimax`. Relative overrides are refused
|
|
437
|
+
* because a background proxy and a foreground client can have different CWDs.
|
|
438
|
+
*/
|
|
439
|
+
export function mcodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
440
|
+
const primary = env.MINIMAX_DATA_DIR?.trim();
|
|
441
|
+
if (primary) return absoluteClientPath(primary, home, "MINIMAX_DATA_DIR");
|
|
442
|
+
const legacy = env.MAVIS_DATA_DIR?.trim();
|
|
443
|
+
if (legacy) return absoluteClientPath(legacy, home, "MAVIS_DATA_DIR");
|
|
444
|
+
return join(home, ".minimax");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function mcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
448
|
+
return join(mcodeHomeDir(env, home), "config.yaml");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* ZCode (Z.ai's desktop client) keeps everything under `~/.zcode`; custom
|
|
453
|
+
* providers live in `v2/config.json`. `ZCODE_DATA_DIR` mirrors the other
|
|
454
|
+
* clients' override convention; relative overrides are refused for the same
|
|
455
|
+
* reason as MCode's.
|
|
456
|
+
*/
|
|
457
|
+
export function zcodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
458
|
+
const override = env.ZCODE_DATA_DIR?.trim();
|
|
459
|
+
if (override) return absoluteClientPath(override, home, "ZCODE_DATA_DIR");
|
|
460
|
+
return join(home, ".zcode");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
464
|
+
return join(zcodeHomeDir(env, home), "v2", "config.json");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Prime Agent resolves its agent directory from `PRIME_AGENT_CODING_AGENT_DIR`
|
|
469
|
+
* — the brand-derived spelling of the `PI_CODING_AGENT_DIR` that `ompAgentDir`
|
|
470
|
+
* already honors, because the agent builds that variable name from its own
|
|
471
|
+
* `piConfig.name` — and otherwise falls back to `~/.prime/agent`. Relative
|
|
472
|
+
* overrides are refused for the same reason as MCode's and ZCode's: a
|
|
473
|
+
* background proxy and a foreground client can have different working
|
|
474
|
+
* directories.
|
|
475
|
+
*/
|
|
476
|
+
export function primeAgentDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
477
|
+
const override = env.PRIME_AGENT_CODING_AGENT_DIR?.trim();
|
|
478
|
+
if (override) return absoluteClientPath(override, home, "PRIME_AGENT_CODING_AGENT_DIR");
|
|
479
|
+
return join(home, ".prime", "agent");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Prime Agent's canonical custom-provider catalog. */
|
|
483
|
+
export function primeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
484
|
+
return join(primeAgentDir(env, home), "models.json");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function clineHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
488
|
+
const override = env.CLINE_DATA_DIR?.trim();
|
|
489
|
+
if (override) return absoluteClientPath(override, home, "CLINE_DATA_DIR");
|
|
490
|
+
|
|
491
|
+
const vscodeDir = process.platform === "darwin"
|
|
492
|
+
? join(home, "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings")
|
|
493
|
+
: process.platform === "win32"
|
|
494
|
+
? join(env.APPDATA || join(home, "AppData", "Roaming"), "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings")
|
|
495
|
+
: join(home, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings");
|
|
496
|
+
|
|
497
|
+
if (existsSync(vscodeDir)) return vscodeDir;
|
|
498
|
+
|
|
499
|
+
const cliSettingsDir = join(home, ".cline", "data", "settings");
|
|
500
|
+
if (existsSync(cliSettingsDir) || existsSync(join(home, ".cline"))) return cliSettingsDir;
|
|
501
|
+
|
|
502
|
+
return vscodeDir;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Cline's canonical providers configuration file. */
|
|
506
|
+
export function clineConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
507
|
+
return join(clineHomeDir(env, home), "providers.json");
|
|
508
|
+
}
|
|
509
|
+
|
|
132
510
|
/**
|
|
133
511
|
* One proxy-routed model destined for a client config. Deliberately narrower than
|
|
134
512
|
* `CatalogModel` so a serializer cannot reach for a field that does not survive the
|
|
@@ -144,6 +522,9 @@ export interface ExportModel {
|
|
|
144
522
|
displayName?: string;
|
|
145
523
|
contextWindow?: number;
|
|
146
524
|
inputModalities?: string[];
|
|
525
|
+
/** Optional effort ladder exported only to clients that support it. */
|
|
526
|
+
reasoningEfforts?: string[];
|
|
527
|
+
defaultReasoningEffort?: string;
|
|
147
528
|
}
|
|
148
529
|
|
|
149
530
|
export interface ExportContext {
|
|
@@ -157,7 +538,19 @@ export interface ExportContext {
|
|
|
157
538
|
config?: OcxConfig;
|
|
158
539
|
}
|
|
159
540
|
|
|
160
|
-
export type ExportClientId =
|
|
541
|
+
export type ExportClientId =
|
|
542
|
+
| "opencode"
|
|
543
|
+
| "pi"
|
|
544
|
+
| "omp"
|
|
545
|
+
| "hermes"
|
|
546
|
+
| "openclaw"
|
|
547
|
+
| "kimi"
|
|
548
|
+
| "gajae"
|
|
549
|
+
| "dsh"
|
|
550
|
+
| "mcode"
|
|
551
|
+
| "zcode"
|
|
552
|
+
| "prime"
|
|
553
|
+
| "cline";
|
|
161
554
|
|
|
162
555
|
export interface ExportClientSpec {
|
|
163
556
|
id: ExportClientId;
|
|
@@ -170,6 +563,34 @@ export interface ExportClientSpec {
|
|
|
170
563
|
/** Shell line the user runs before launching the client. */
|
|
171
564
|
exportHint: string;
|
|
172
565
|
build: (ctx: ExportContext) => unknown;
|
|
566
|
+
/**
|
|
567
|
+
* Text format of the client's config file. `filename` already carries the
|
|
568
|
+
* extension; this drives serialization and the download media type so no
|
|
569
|
+
* consumer has to infer either from the name.
|
|
570
|
+
*/
|
|
571
|
+
format: ConfigFormat;
|
|
572
|
+
/**
|
|
573
|
+
* Count models in THIS client's document shape. Required so a new client
|
|
574
|
+
* cannot be added without teaching the summarizer about it — the old
|
|
575
|
+
* "anything that is not OpenCode must be Pi" branch was a latent bug.
|
|
576
|
+
*/
|
|
577
|
+
summarize: (document: unknown) => { modelCount: number; modelsWithoutLimits: number };
|
|
578
|
+
/**
|
|
579
|
+
* The fragments opencodex owns inside this client's config. Only the builder
|
|
580
|
+
* knows where a client keeps our entries, so ownership paths originate here
|
|
581
|
+
* rather than being re-derived by the writer.
|
|
582
|
+
*/
|
|
583
|
+
buildContribution: BuildContribution;
|
|
584
|
+
/**
|
|
585
|
+
* True when the generated integration deliberately supports loopback only.
|
|
586
|
+
*
|
|
587
|
+
* `/v1/chat/completions` rejects bearer credentials and requires the
|
|
588
|
+
* dedicated `x-opencodex-api-key` header (AUTH_MATRIX in
|
|
589
|
+
* src/server/auth-cors.ts). If this exporter cannot safely emit that header,
|
|
590
|
+
* it refuses a remote bind rather than generating a config that 401s. Same
|
|
591
|
+
* reasoning as the Grok managed block's non-loopback refusal.
|
|
592
|
+
*/
|
|
593
|
+
loopbackOnly: boolean;
|
|
173
594
|
}
|
|
174
595
|
|
|
175
596
|
/**
|
|
@@ -178,7 +599,8 @@ export interface ExportClientSpec {
|
|
|
178
599
|
*/
|
|
179
600
|
function authoritativeContextWindow(contextWindow: number | undefined): number | undefined {
|
|
180
601
|
if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
|
|
181
|
-
|
|
602
|
+
const integer = Math.floor(contextWindow);
|
|
603
|
+
return integer > 0 ? integer : undefined;
|
|
182
604
|
}
|
|
183
605
|
return undefined;
|
|
184
606
|
}
|
|
@@ -188,6 +610,67 @@ function outputBudgetFor(context: number): number {
|
|
|
188
610
|
return Math.min(SCHEMA_REQUIRED_OUTPUT_BUDGET, context);
|
|
189
611
|
}
|
|
190
612
|
|
|
613
|
+
/**
|
|
614
|
+
* Modalities a given client's schema will actually accept.
|
|
615
|
+
*
|
|
616
|
+
* Our internal vocabulary is `text | image | audio` (ALLOWED_INPUT_MODALITIES in
|
|
617
|
+
* src/server/management/model-routes.ts). Pi and Gajae accept only
|
|
618
|
+
* `text | image`, and both reject the WHOLE config file over one out-of-enum
|
|
619
|
+
* value — Gajae reports `/providers/opencodex/models/N/input/2: Invalid option`
|
|
620
|
+
* and falls back to its built-in list, Pi returns an empty model config. So a
|
|
621
|
+
* single `audio` model takes every routed model down with it. That is not
|
|
622
|
+
* hypothetical: zenmux/meta-muse-spark-1.1 advertises audio and did exactly
|
|
623
|
+
* this. It is also the same defect the Codex catalog had with `video`, where
|
|
624
|
+
* the app showed zero apps (tests/catalog-input-modality-enum.test.ts).
|
|
625
|
+
*
|
|
626
|
+
* UNKNOWN and INCOMPATIBLE are different inputs, and the Codex fix could
|
|
627
|
+
* conflate them safely only because its enum is wider. A model with nothing
|
|
628
|
+
* declared is unknown, and `text` is the honest floor — every routed model takes
|
|
629
|
+
* prompts. A model declaring `["audio"]` and nothing else is incompatible with a
|
|
630
|
+
* text|image client, and rewriting it to `["text"]` would advertise a capability
|
|
631
|
+
* it does not have. That input is reachable three ways: `ocx models add
|
|
632
|
+
* --modalities audio`, `/api/custom-models`, and provider discovery.
|
|
633
|
+
*
|
|
634
|
+
* So unknown falls back to text and incompatible returns null, which drops the
|
|
635
|
+
* row. Omitting a model costs the user a line in a picker; fabricating `text`
|
|
636
|
+
* costs them a model that fails at call time with no explanation.
|
|
637
|
+
*
|
|
638
|
+
* Deliberately NOT applied in `ExportModel` construction: the management and CLI
|
|
639
|
+
* boundaries carry catalog modalities verbatim on purpose, and stripping `audio`
|
|
640
|
+
* globally would destroy valid metadata before the destination is known.
|
|
641
|
+
*/
|
|
642
|
+
const CLIENT_INPUT_MODALITIES: Record<"pi" | "gajae", ReadonlySet<string>> = {
|
|
643
|
+
pi: new Set(["text", "image"]),
|
|
644
|
+
gajae: new Set(["text", "image"]),
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
/** `null` means the model cannot be represented for this client — drop the row. */
|
|
648
|
+
function inputModalitiesForClient(
|
|
649
|
+
client: "pi" | "gajae",
|
|
650
|
+
modalities: readonly string[] | undefined,
|
|
651
|
+
): string[] | null {
|
|
652
|
+
const declared = modalities ?? [];
|
|
653
|
+
if (declared.length === 0) return ["text"];
|
|
654
|
+
const accepted = CLIENT_INPUT_MODALITIES[client];
|
|
655
|
+
const kept: string[] = [];
|
|
656
|
+
for (const value of declared) {
|
|
657
|
+
if (accepted.has(value) && !kept.includes(value)) kept.push(value);
|
|
658
|
+
}
|
|
659
|
+
return kept.length > 0 ? kept : null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** DSH rc.6 accepts text/image; unknown values degrade to text, while audio-only cannot be represented. */
|
|
663
|
+
function dshInputModalities(modalities: readonly string[] | undefined): string[] | null {
|
|
664
|
+
const declared = modalities ?? [];
|
|
665
|
+
if (declared.length === 0) return ["text"];
|
|
666
|
+
const kept: string[] = [];
|
|
667
|
+
for (const value of declared) {
|
|
668
|
+
if ((value === "text" || value === "image") && !kept.includes(value)) kept.push(value);
|
|
669
|
+
}
|
|
670
|
+
if (kept.length > 0) return kept;
|
|
671
|
+
return declared.every(value => value === "audio") ? null : ["text"];
|
|
672
|
+
}
|
|
673
|
+
|
|
191
674
|
/**
|
|
192
675
|
* Label shared by every client: `"<displayName|id> (<native|provider|routed>)"`. The
|
|
193
676
|
* provider suffix is what makes two same-named models from different upstreams
|
|
@@ -266,10 +749,10 @@ export function buildOpencodeProviderBlockFromCatalog(
|
|
|
266
749
|
* models produce identical bytes. Stability matters because the GUI shows a diffable
|
|
267
750
|
* preview and agents may checksum the payload.
|
|
268
751
|
*/
|
|
269
|
-
export function normalizeExportModels(models: readonly ExportModel[]): ExportModel[] {
|
|
752
|
+
export function normalizeExportModels(models: readonly ExportModel[] = []): ExportModel[] {
|
|
270
753
|
const seen = new Set<string>();
|
|
271
754
|
const unique: ExportModel[] = [];
|
|
272
|
-
for (const model of models) {
|
|
755
|
+
for (const model of models ?? []) {
|
|
273
756
|
if (seen.has(model.namespaced)) continue;
|
|
274
757
|
seen.add(model.namespaced);
|
|
275
758
|
unique.push(model);
|
|
@@ -293,6 +776,15 @@ export interface PiModelEntry {
|
|
|
293
776
|
input: string[];
|
|
294
777
|
contextWindow?: number;
|
|
295
778
|
maxTokens?: number;
|
|
779
|
+
/** Advertised when the catalog row carries a non-empty effort ladder. */
|
|
780
|
+
reasoning?: true;
|
|
781
|
+
/**
|
|
782
|
+
* Constrains pi's own level scale (minimal..max) to the declared ladder: members map to
|
|
783
|
+
* themselves, everything else is hidden (`null`). Without it pi would offer levels the
|
|
784
|
+
* ladder does not contain — harmless for provider-config ladders (the proxy clamps those
|
|
785
|
+
* at the wire) but a real 400 risk for custom-row ladders, which are advertisement-only.
|
|
786
|
+
*/
|
|
787
|
+
thinkingLevelMap?: Record<string, string | null>;
|
|
296
788
|
}
|
|
297
789
|
|
|
298
790
|
export interface PiProviderBlock {
|
|
@@ -306,46 +798,800 @@ export interface PiGeneratedConfig {
|
|
|
306
798
|
providers: Record<string, PiProviderBlock>;
|
|
307
799
|
}
|
|
308
800
|
|
|
801
|
+
/**
|
|
802
|
+
* omp accepts a model-level API override. Keep the provider on Chat
|
|
803
|
+
* Completions so routed providers retain their established wire format, while
|
|
804
|
+
* native OpenAI models can use the lossless Responses surface.
|
|
805
|
+
*/
|
|
806
|
+
export interface OmpModelEntry extends PiModelEntry {
|
|
807
|
+
api?: "openai-responses";
|
|
808
|
+
/** omp requires this flag before it honors a thinking block. */
|
|
809
|
+
reasoning?: true;
|
|
810
|
+
thinking?: {
|
|
811
|
+
mode: "effort";
|
|
812
|
+
efforts: string[];
|
|
813
|
+
defaultLevel?: string;
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export interface OmpProviderBlock {
|
|
818
|
+
baseUrl: string;
|
|
819
|
+
api: typeof PI_API_DIALECT;
|
|
820
|
+
apiKey: string;
|
|
821
|
+
models: OmpModelEntry[];
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export interface OmpGeneratedConfig {
|
|
825
|
+
providers: Record<string, OmpProviderBlock>;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* omp validates model entries strictly. These are its documented effort
|
|
830
|
+
* values; omit an unknown value rather than invalidating the whole provider.
|
|
831
|
+
*/
|
|
832
|
+
const OMP_EFFORT_VOCABULARY = new Set(["minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
833
|
+
|
|
834
|
+
function ompEfforts(model: ExportModel): string[] {
|
|
835
|
+
const efforts: string[] = [];
|
|
836
|
+
for (const effort of model.reasoningEfforts ?? []) {
|
|
837
|
+
const normalized = effort.trim().toLowerCase();
|
|
838
|
+
if (OMP_EFFORT_VOCABULARY.has(normalized) && !efforts.includes(normalized)) {
|
|
839
|
+
efforts.push(normalized);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return efforts;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Hermes `~/.hermes/config.yaml`. We emit ONLY the provider entry — never
|
|
847
|
+
* `model.default` — because hijacking the user's main model is not what a
|
|
848
|
+
* connect action asks for.
|
|
849
|
+
*/
|
|
850
|
+
export interface HermesProviderBlock {
|
|
851
|
+
api: string;
|
|
852
|
+
api_key: string;
|
|
853
|
+
api_mode: "chat_completions";
|
|
854
|
+
/** We supply the list, so skip their live `/models` probe. */
|
|
855
|
+
discover_models: false;
|
|
856
|
+
models: string[];
|
|
857
|
+
extra_headers?: Record<string, string>;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export interface HermesGeneratedConfig {
|
|
861
|
+
providers: Record<string, HermesProviderBlock>;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export interface OpenclawModelEntry {
|
|
865
|
+
id: string;
|
|
866
|
+
name: string;
|
|
867
|
+
contextWindow?: number;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
export interface OpenclawProviderBlock {
|
|
871
|
+
baseUrl: string;
|
|
872
|
+
apiKey: string;
|
|
873
|
+
api: "openai-completions";
|
|
874
|
+
models: OpenclawModelEntry[];
|
|
875
|
+
headers?: Record<string, string>;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** `mode: "merge"` keeps OpenClaw's bundled catalog alongside ours. */
|
|
879
|
+
export interface OpenclawGeneratedConfig {
|
|
880
|
+
models: {
|
|
881
|
+
mode: "merge";
|
|
882
|
+
providers: Record<string, OpenclawProviderBlock>;
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
export interface KimiProviderBlock {
|
|
887
|
+
type: "openai";
|
|
888
|
+
base_url: string;
|
|
889
|
+
api_key: string;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* `max_context_size` is mandatory and must be positive, so a model with no
|
|
894
|
+
* authoritative context window is omitted from the document entirely rather
|
|
895
|
+
* than guessed at. `capabilities` is never emitted: our catalog does not
|
|
896
|
+
* assert them, and Kimi's own inference works off OpenAI-style name prefixes
|
|
897
|
+
* that a routed selector will not match.
|
|
898
|
+
*/
|
|
899
|
+
export interface KimiModelBlock {
|
|
900
|
+
provider: string;
|
|
901
|
+
model: string;
|
|
902
|
+
max_context_size: number;
|
|
903
|
+
display_name?: string;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
export interface KimiGeneratedConfig {
|
|
907
|
+
providers: Record<string, KimiProviderBlock>;
|
|
908
|
+
models: Record<string, KimiModelBlock>;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
export interface GajaeModelEntry {
|
|
912
|
+
id: string;
|
|
913
|
+
name: string;
|
|
914
|
+
input: string[];
|
|
915
|
+
contextWindow?: number;
|
|
916
|
+
maxTokens?: number;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Gajae validates strictly: an unknown field fails the whole config. */
|
|
920
|
+
export interface GajaeProviderBlock {
|
|
921
|
+
baseUrl: string;
|
|
922
|
+
apiKeyEnv: string;
|
|
923
|
+
api: "openai-completions";
|
|
924
|
+
models: GajaeModelEntry[];
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export interface GajaeGeneratedConfig {
|
|
928
|
+
providers: Record<string, GajaeProviderBlock>;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export type DshReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
|
932
|
+
export type DshWireReasoningEffort = DshReasoningEffort | "ultra";
|
|
933
|
+
|
|
934
|
+
export interface DshModelEntry {
|
|
935
|
+
id: string;
|
|
936
|
+
name: string;
|
|
937
|
+
input: string[];
|
|
938
|
+
contextWindow?: number;
|
|
939
|
+
reasoningEfforts?: Partial<Record<DshReasoningEffort, DshWireReasoningEffort>>;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export interface DshProviderBlock {
|
|
943
|
+
displayName: "OpenCodex";
|
|
944
|
+
api: "openai-responses";
|
|
945
|
+
baseURL: string;
|
|
946
|
+
headers: { Authorization: "Bearer ocx_data_dsh" };
|
|
947
|
+
models: DshModelEntry[];
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
export interface DshGeneratedConfig {
|
|
951
|
+
"llm-pi-ai": {
|
|
952
|
+
providers: Record<string, DshProviderBlock>;
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
export interface McodeProviderBlock {
|
|
957
|
+
name: "OpenCodex";
|
|
958
|
+
kind: "custom";
|
|
959
|
+
enabled: true;
|
|
960
|
+
api: "anthropic-messages";
|
|
961
|
+
options: {
|
|
962
|
+
apiKey: string;
|
|
963
|
+
baseURL: string;
|
|
964
|
+
authMode: "api-key";
|
|
965
|
+
};
|
|
966
|
+
models: Record<string, McodeModelEntry>;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
export interface McodeModelEntry {
|
|
970
|
+
/** MCode uses this value for context accounting and compaction. */
|
|
971
|
+
limit?: { context: number };
|
|
972
|
+
/** MCode exposes these exact levels in `/model` and sends the selected effort. */
|
|
973
|
+
thinking?: { effortOptions: string[] };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
export interface McodeGeneratedConfig {
|
|
977
|
+
custom_provider: Record<string, McodeProviderBlock>;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* ZCode's `~/.zcode/v2/config.json` provider entry (observed schema, validated
|
|
982
|
+
* live against ZCode 3.7.7 / 3.8.1). `kind: "openai-compatible"` selects the
|
|
983
|
+
* OpenAI Chat Completions protocol, which the proxy serves at `/v1/chat/completions`.
|
|
984
|
+
* `apiKeyRequired` keeps ZCode's UI from prompting for a key it does not need on
|
|
985
|
+
* loopback; the serialized key is always the non-secret loopback placeholder.
|
|
986
|
+
*/
|
|
987
|
+
export interface ZcodeModelEntry {
|
|
988
|
+
name?: string;
|
|
989
|
+
limit?: { context: number; output?: number };
|
|
990
|
+
modalities: { input: string[]; output: string[] };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
export interface ZcodeProviderBlock {
|
|
994
|
+
name: "OpenCodex";
|
|
995
|
+
kind: "openai-compatible";
|
|
996
|
+
enabled: true;
|
|
997
|
+
source: "custom";
|
|
998
|
+
options: {
|
|
999
|
+
apiKey: string;
|
|
1000
|
+
baseURL: string;
|
|
1001
|
+
apiKeyRequired: true;
|
|
1002
|
+
};
|
|
1003
|
+
models: Record<string, ZcodeModelEntry>;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
export interface ZcodeGeneratedConfig {
|
|
1007
|
+
provider: Record<string, ZcodeProviderBlock>;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
309
1010
|
/**
|
|
310
1011
|
* Pi's `~/.pi/agent/models.json` shape. `models` is an ARRAY (identity lives in `id`),
|
|
311
1012
|
* unlike OpenCode's keyed object.
|
|
312
1013
|
*
|
|
313
|
-
* Two fields
|
|
314
|
-
* no price data at all, so emitting zeros would assert every routed
|
|
315
|
-
*
|
|
316
|
-
*
|
|
1014
|
+
* Two fields were deliberately absent once. `cost` still is: it requires all four price
|
|
1015
|
+
* fields and we have no price data at all, so emitting zeros would assert every routed
|
|
1016
|
+
* model is free. `reasoning` used to be omitted because Pi's boolean and the catalog's
|
|
1017
|
+
* effort ladder did not obviously map — but a NON-EMPTY ladder is the catalog's own
|
|
1018
|
+
* statement that the model accepts reasoning parameters (adapters honor `reasoning_effort`),
|
|
1019
|
+
* and an empty or absent ladder is the statement that it does not. Emitting `reasoning:
|
|
1020
|
+
* true` exactly for rows with a ladder is therefore not a guess; it is what makes Pi's
|
|
1021
|
+
* effort control appear for routed models at all. The export also emits a `thinkingLevelMap`
|
|
1022
|
+
* that hides every pi level outside the declared ladder, so pi never offers (and sends) an
|
|
1023
|
+
* effort the ladder does not contain — custom-row ladders are catalog advertisement only
|
|
1024
|
+
* and get no wire clamp, so this map is what keeps pi honest for those. Users who need a
|
|
1025
|
+
* different mapping can still hand-tune `thinkingLevelMap` afterwards.
|
|
317
1026
|
*
|
|
318
|
-
* Pi's
|
|
319
|
-
*
|
|
1027
|
+
* Pi's input enum IS verified: its documented model configuration accepts only
|
|
1028
|
+
* `text` and `image`, and a validation failure yields an EMPTY model config
|
|
1029
|
+
* rather than dropping the offending entry — one bad value costs every routed
|
|
1030
|
+
* model. The rest of this contract (omitting `cost`) is still ours rather than
|
|
1031
|
+
* a claim about Pi's acceptance.
|
|
320
1032
|
*/
|
|
321
1033
|
function buildPiClientConfig(ctx: ExportContext): PiGeneratedConfig {
|
|
322
|
-
const models: PiModelEntry[] =
|
|
1034
|
+
const models: PiModelEntry[] = [];
|
|
1035
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
1036
|
+
// Text is the one modality every routed model supports; anything richer must come
|
|
1037
|
+
// from the catalog rather than an assumption — and must still be inside the enum
|
|
1038
|
+
// Pi accepts, because one rejected value empties the whole config.
|
|
1039
|
+
const input = inputModalitiesForClient("pi", model.inputModalities);
|
|
1040
|
+
// An audio-only model has no honest representation here; claiming `text`
|
|
1041
|
+
// would fail at call time instead, so the row is dropped.
|
|
1042
|
+
if (input === null) continue;
|
|
323
1043
|
const entry: PiModelEntry = {
|
|
324
1044
|
id: model.namespaced,
|
|
325
1045
|
name: exportModelLabel(model),
|
|
326
|
-
|
|
327
|
-
// from the catalog rather than an assumption.
|
|
328
|
-
input: model.inputModalities && model.inputModalities.length > 0 ? [...model.inputModalities] : ["text"],
|
|
1046
|
+
input,
|
|
329
1047
|
};
|
|
1048
|
+
if (Array.isArray(model.reasoningEfforts) && model.reasoningEfforts.length > 0) {
|
|
1049
|
+
entry.reasoning = true;
|
|
1050
|
+
const efforts = model.reasoningEfforts;
|
|
1051
|
+
entry.thinkingLevelMap = {
|
|
1052
|
+
// pi's off level maps to the declared `none` sentinel (the proxy omits the
|
|
1053
|
+
// reasoning parameter for it); hidden when the ladder does not declare none.
|
|
1054
|
+
off: efforts.includes("none") ? "none" : null,
|
|
1055
|
+
minimal: efforts.includes("minimal") ? "minimal" : null,
|
|
1056
|
+
low: efforts.includes("low") ? "low" : null,
|
|
1057
|
+
medium: efforts.includes("medium") ? "medium" : null,
|
|
1058
|
+
high: efforts.includes("high") ? "high" : null,
|
|
1059
|
+
xhigh: efforts.includes("xhigh") ? "xhigh" : null,
|
|
1060
|
+
max: efforts.includes("max") ? "max" : efforts.includes("ultra") ? "ultra" : null,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
330
1063
|
const context = authoritativeContextWindow(model.contextWindow);
|
|
331
1064
|
if (context !== undefined) {
|
|
332
1065
|
entry.contextWindow = context;
|
|
333
1066
|
entry.maxTokens = outputBudgetFor(context);
|
|
334
1067
|
}
|
|
335
|
-
|
|
336
|
-
}
|
|
1068
|
+
models.push(entry);
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
providers: {
|
|
1072
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
1073
|
+
baseUrl: ctx.baseUrl,
|
|
1074
|
+
api: PI_API_DIALECT,
|
|
1075
|
+
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
1076
|
+
models,
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* omp's models.yml is Pi-like, but it supports effort metadata and a per-model
|
|
1084
|
+
* API dialect. Native OpenAI models use Responses; all routed models inherit
|
|
1085
|
+
* the provider's existing Chat Completions dialect.
|
|
1086
|
+
*/
|
|
1087
|
+
function buildOmpClientConfig(ctx: ExportContext): OmpGeneratedConfig {
|
|
1088
|
+
const models: OmpModelEntry[] = [];
|
|
1089
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
1090
|
+
const input = inputModalitiesForClient("pi", model.inputModalities);
|
|
1091
|
+
if (input === null) continue;
|
|
1092
|
+
const entry: OmpModelEntry = {
|
|
1093
|
+
id: model.namespaced,
|
|
1094
|
+
name: exportModelLabel(model),
|
|
1095
|
+
input,
|
|
1096
|
+
...(model.native && model.provider === "openai" ? { api: "openai-responses" } : {}),
|
|
1097
|
+
};
|
|
1098
|
+
const context = authoritativeContextWindow(model.contextWindow);
|
|
1099
|
+
if (context !== undefined) {
|
|
1100
|
+
entry.contextWindow = context;
|
|
1101
|
+
entry.maxTokens = outputBudgetFor(context);
|
|
1102
|
+
}
|
|
1103
|
+
const efforts = ompEfforts(model);
|
|
1104
|
+
if (efforts.length > 0) {
|
|
1105
|
+
const defaultLevel = model.defaultReasoningEffort?.trim().toLowerCase();
|
|
1106
|
+
entry.reasoning = true;
|
|
1107
|
+
entry.thinking = {
|
|
1108
|
+
mode: "effort",
|
|
1109
|
+
efforts,
|
|
1110
|
+
...(defaultLevel && efforts.includes(defaultLevel) ? { defaultLevel } : {}),
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
models.push(entry);
|
|
1114
|
+
}
|
|
337
1115
|
return {
|
|
338
1116
|
providers: {
|
|
339
1117
|
[OPENCODE_PROVIDER_ID]: {
|
|
340
1118
|
baseUrl: ctx.baseUrl,
|
|
341
1119
|
api: PI_API_DIALECT,
|
|
342
|
-
apiKey:
|
|
1120
|
+
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
1121
|
+
models,
|
|
1122
|
+
},
|
|
1123
|
+
},
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/** Extra headers a non-loopback bind needs, or nothing on loopback. */
|
|
1128
|
+
function proxyAdmissionHeaders(config: OcxConfig | undefined, envRef: string): Record<string, string> | undefined {
|
|
1129
|
+
return shouldInjectApiAuthHeader(config) ? { "x-opencodex-api-key": envRef } : undefined;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function buildHermesClientConfig(ctx: ExportContext): HermesGeneratedConfig {
|
|
1133
|
+
const models = normalizeExportModels(ctx.models).map(model => model.namespaced);
|
|
1134
|
+
const headers = proxyAdmissionHeaders(ctx.config, HERMES_API_KEY_ENV_REF);
|
|
1135
|
+
return {
|
|
1136
|
+
providers: {
|
|
1137
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
1138
|
+
api: ctx.baseUrl,
|
|
1139
|
+
api_key: HERMES_API_KEY_ENV_REF,
|
|
1140
|
+
api_mode: "chat_completions",
|
|
1141
|
+
discover_models: false,
|
|
1142
|
+
models,
|
|
1143
|
+
...(headers ? { extra_headers: headers } : {}),
|
|
1144
|
+
},
|
|
1145
|
+
},
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function buildOpenclawClientConfig(ctx: ExportContext): OpenclawGeneratedConfig {
|
|
1150
|
+
const models: OpenclawModelEntry[] = normalizeExportModels(ctx.models).map(model => {
|
|
1151
|
+
const context = authoritativeContextWindow(model.contextWindow);
|
|
1152
|
+
return {
|
|
1153
|
+
id: model.namespaced,
|
|
1154
|
+
name: exportModelLabel(model),
|
|
1155
|
+
...(context !== undefined ? { contextWindow: context } : {}),
|
|
1156
|
+
};
|
|
1157
|
+
});
|
|
1158
|
+
const headers = proxyAdmissionHeaders(ctx.config, OPENCLAW_API_KEY_ENV_REF);
|
|
1159
|
+
return {
|
|
1160
|
+
models: {
|
|
1161
|
+
mode: "merge",
|
|
1162
|
+
providers: {
|
|
1163
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
1164
|
+
baseUrl: ctx.baseUrl,
|
|
1165
|
+
apiKey: OPENCLAW_API_KEY_ENV_REF,
|
|
1166
|
+
api: "openai-completions",
|
|
1167
|
+
models,
|
|
1168
|
+
...(headers ? { headers } : {}),
|
|
1169
|
+
},
|
|
1170
|
+
},
|
|
1171
|
+
},
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/** Kimi's model alias: one key per model, namespaced under our provider id. */
|
|
1176
|
+
export function kimiModelAlias(namespaced: string): string {
|
|
1177
|
+
return `${OPENCODE_PROVIDER_ID}/${namespaced}`;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function buildKimiClientConfig(ctx: ExportContext): KimiGeneratedConfig {
|
|
1181
|
+
const models: Record<string, KimiModelBlock> = {};
|
|
1182
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
1183
|
+
const context = authoritativeContextWindow(model.contextWindow);
|
|
1184
|
+
// `max_context_size` is mandatory and must be positive. We do not guess it,
|
|
1185
|
+
// so a model without an authoritative window is left out rather than
|
|
1186
|
+
// shipped with a number we invented.
|
|
1187
|
+
if (context === undefined) continue;
|
|
1188
|
+
models[kimiModelAlias(model.namespaced)] = {
|
|
1189
|
+
provider: OPENCODE_PROVIDER_ID,
|
|
1190
|
+
model: model.namespaced,
|
|
1191
|
+
max_context_size: context,
|
|
1192
|
+
...(model.displayName ? { display_name: model.displayName } : {}),
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
return {
|
|
1196
|
+
providers: {
|
|
1197
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
1198
|
+
type: "openai",
|
|
1199
|
+
base_url: ctx.baseUrl,
|
|
1200
|
+
api_key: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
1201
|
+
},
|
|
1202
|
+
},
|
|
1203
|
+
models,
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function buildGajaeClientConfig(ctx: ExportContext): GajaeGeneratedConfig {
|
|
1208
|
+
const models: GajaeModelEntry[] = [];
|
|
1209
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
1210
|
+
// Gajae's enum is text|image and it rejects the whole file over one bad
|
|
1211
|
+
// value, naming the offending index in the error.
|
|
1212
|
+
const input = inputModalitiesForClient("gajae", model.inputModalities);
|
|
1213
|
+
if (input === null) continue;
|
|
1214
|
+
const entry: GajaeModelEntry = {
|
|
1215
|
+
id: model.namespaced,
|
|
1216
|
+
name: exportModelLabel(model),
|
|
1217
|
+
input,
|
|
1218
|
+
};
|
|
1219
|
+
const context = authoritativeContextWindow(model.contextWindow);
|
|
1220
|
+
if (context !== undefined) {
|
|
1221
|
+
entry.contextWindow = context;
|
|
1222
|
+
entry.maxTokens = outputBudgetFor(context);
|
|
1223
|
+
}
|
|
1224
|
+
models.push(entry);
|
|
1225
|
+
}
|
|
1226
|
+
return {
|
|
1227
|
+
providers: {
|
|
1228
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
1229
|
+
baseUrl: ctx.baseUrl,
|
|
1230
|
+
apiKeyEnv: GAJAE_API_KEY_ENV,
|
|
1231
|
+
api: "openai-completions",
|
|
1232
|
+
models,
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const DSH_EFFORT_ORDER: readonly DshReasoningEffort[] = ["low", "medium", "high", "xhigh", "max"];
|
|
1239
|
+
|
|
1240
|
+
function dshReasoningEfforts(model: ExportModel): DshModelEntry["reasoningEfforts"] {
|
|
1241
|
+
const offered = new Set<string>();
|
|
1242
|
+
for (const raw of model.reasoningEfforts ?? []) {
|
|
1243
|
+
const effort = raw.trim().toLowerCase();
|
|
1244
|
+
if (effort === "ultra" || DSH_EFFORT_ORDER.includes(effort as DshReasoningEffort)) offered.add(effort);
|
|
1245
|
+
}
|
|
1246
|
+
if (offered.size === 0) return undefined;
|
|
1247
|
+
const entries: Array<[DshReasoningEffort, DshWireReasoningEffort]> = [];
|
|
1248
|
+
for (const effort of DSH_EFFORT_ORDER) {
|
|
1249
|
+
if (effort !== "max") {
|
|
1250
|
+
if (offered.has(effort)) entries.push([effort, effort]);
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
// DSH's key is the selectable level; the value is what it sends on the
|
|
1254
|
+
// wire. Preserve OpenCodex's `ultra` spelling when that is the only
|
|
1255
|
+
// highest effort, exactly like the rc.6 `max: ultra` contract.
|
|
1256
|
+
if (offered.has("max")) entries.push(["max", "max"]);
|
|
1257
|
+
else if (offered.has("ultra")) entries.push(["max", "ultra"]);
|
|
1258
|
+
}
|
|
1259
|
+
return Object.fromEntries(entries);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function isKnownSafeDshCombo(model: ExportModel, config: OcxConfig): boolean {
|
|
1263
|
+
const combos = (config as { combos?: unknown }).combos;
|
|
1264
|
+
if (typeof combos !== "object" || combos === null || Array.isArray(combos)) return false;
|
|
1265
|
+
const combo = (combos as Record<string, unknown>)[model.id];
|
|
1266
|
+
if (typeof combo !== "object" || combo === null || Array.isArray(combo)) return false;
|
|
1267
|
+
const targets = (combo as { targets?: unknown }).targets;
|
|
1268
|
+
if (!Array.isArray(targets) || targets.length === 0) return false;
|
|
1269
|
+
return targets.every(target => {
|
|
1270
|
+
if (typeof target !== "object" || target === null || Array.isArray(target)) return false;
|
|
1271
|
+
const provider = (target as { provider?: unknown }).provider;
|
|
1272
|
+
const modelId = (target as { model?: unknown }).model;
|
|
1273
|
+
return typeof provider === "string"
|
|
1274
|
+
&& provider.length > 0
|
|
1275
|
+
&& provider === provider.trim()
|
|
1276
|
+
&& provider !== "openai"
|
|
1277
|
+
&& typeof modelId === "string"
|
|
1278
|
+
&& modelId.length > 0
|
|
1279
|
+
&& modelId === modelId.trim();
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function buildDshClientConfig(ctx: ExportContext): DshGeneratedConfig {
|
|
1284
|
+
const direct = providerCodexAccountMode("openai", ctx.config?.providers?.openai) === "direct";
|
|
1285
|
+
const models: DshModelEntry[] = [];
|
|
1286
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
1287
|
+
if (direct && (model.native === true || model.provider === "openai")) continue;
|
|
1288
|
+
if (direct && model.provider === "combo" && (!ctx.config || !isKnownSafeDshCombo(model, ctx.config))) continue;
|
|
1289
|
+
const input = dshInputModalities(model.inputModalities);
|
|
1290
|
+
if (input === null) continue;
|
|
1291
|
+
const contextWindow = authoritativeContextWindow(model.contextWindow);
|
|
1292
|
+
const reasoningEfforts = dshReasoningEfforts(model);
|
|
1293
|
+
models.push({
|
|
1294
|
+
id: model.namespaced,
|
|
1295
|
+
name: exportModelLabel(model),
|
|
1296
|
+
input,
|
|
1297
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
1298
|
+
...(reasoningEfforts ? { reasoningEfforts } : {}),
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
return {
|
|
1302
|
+
"llm-pi-ai": {
|
|
1303
|
+
providers: {
|
|
1304
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
1305
|
+
displayName: "OpenCodex",
|
|
1306
|
+
api: "openai-responses",
|
|
1307
|
+
baseURL: ctx.baseUrl,
|
|
1308
|
+
headers: { Authorization: "Bearer ocx_data_dsh" },
|
|
1309
|
+
models,
|
|
1310
|
+
},
|
|
1311
|
+
},
|
|
1312
|
+
},
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* MiniMax Code's `provider add` command persists custom providers under
|
|
1318
|
+
* `custom_provider.<id>`. Its current model schema reads `limit.context` for
|
|
1319
|
+
* context accounting and `thinking.effortOptions` for the `/model` effort
|
|
1320
|
+
* control. Do not emit the removed `thinking.effort` / `defaultEffort` fields:
|
|
1321
|
+
* MCode 0.1.6 migrates those into options and keeps the selected effort in the
|
|
1322
|
+
* session. Do not emit `defaultModel` either: connecting a client must not
|
|
1323
|
+
* silently replace the user's current model selection.
|
|
1324
|
+
*/
|
|
1325
|
+
function buildMcodeClientConfig(ctx: ExportContext): McodeGeneratedConfig {
|
|
1326
|
+
const models: Record<string, McodeModelEntry> = {};
|
|
1327
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
1328
|
+
const entry: McodeModelEntry = {};
|
|
1329
|
+
const context = authoritativeContextWindow(model.contextWindow);
|
|
1330
|
+
if (context !== undefined) entry.limit = { context };
|
|
1331
|
+
// `none` is an internal Codex catalog sentinel, not an MCode effort. MCode
|
|
1332
|
+
// forwards every option as `output_config.effort` while keeping adaptive
|
|
1333
|
+
// thinking enabled, and the Anthropic ingress deliberately accepts only
|
|
1334
|
+
// minimal..ultra. Advertising `none` would therefore create a selectable
|
|
1335
|
+
// value that cannot disable reasoning and is not forwarded as an effort.
|
|
1336
|
+
const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts)
|
|
1337
|
+
?.filter(effort => effort !== "none");
|
|
1338
|
+
if (efforts && efforts.length > 0) entry.thinking = { effortOptions: efforts };
|
|
1339
|
+
models[model.namespaced] = entry;
|
|
1340
|
+
}
|
|
1341
|
+
return {
|
|
1342
|
+
custom_provider: {
|
|
1343
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
1344
|
+
name: "OpenCodex",
|
|
1345
|
+
kind: "custom",
|
|
1346
|
+
enabled: true,
|
|
1347
|
+
api: "anthropic-messages",
|
|
1348
|
+
options: {
|
|
1349
|
+
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
1350
|
+
baseURL: ctx.baseUrl.replace(/\/v1\/?$/, ""),
|
|
1351
|
+
authMode: "api-key",
|
|
1352
|
+
},
|
|
1353
|
+
models,
|
|
1354
|
+
},
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* ZCode dials the OpenAI Chat Completions surface (`openai-compatible`), which
|
|
1361
|
+
* appends `/chat/completions` to `baseURL`. We supply `baseURL` with the `/v1`
|
|
1362
|
+
* suffix so requests land on `/v1/chat/completions`. Model ids are the proxy's canonical
|
|
1363
|
+
* `provider/id` selectors, which `/v1/chat/completions` resolves directly. Context
|
|
1364
|
+
* limits follow the authoritative-window rule: a model without one ships
|
|
1365
|
+
* without `limit` rather than guessing. Modalities are ZCode's observed
|
|
1366
|
+
* `text`-floor vocabulary; image-capable rows advertise image input.
|
|
1367
|
+
*/
|
|
1368
|
+
function buildZcodeClientConfig(ctx: ExportContext): ZcodeGeneratedConfig {
|
|
1369
|
+
const models: Record<string, ZcodeModelEntry> = {};
|
|
1370
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
1371
|
+
const input = inputModalitiesForClient("pi", model.inputModalities);
|
|
1372
|
+
if (input === null) continue;
|
|
1373
|
+
const entry: ZcodeModelEntry = {
|
|
1374
|
+
name: exportModelLabel(model),
|
|
1375
|
+
modalities: { input, output: ["text"] },
|
|
1376
|
+
};
|
|
1377
|
+
// `limit.context` follows the authoritative-window rule. `output` is
|
|
1378
|
+
// deliberately absent: ZCode's schema makes it optional and we have no
|
|
1379
|
+
// authoritative output budget to assert (reviewer finding: an emitted
|
|
1380
|
+
// stand-in would be a guessed capability, exactly what "no metadata is
|
|
1381
|
+
// guessed" forbids).
|
|
1382
|
+
const context = authoritativeContextWindow(model.contextWindow);
|
|
1383
|
+
if (context !== undefined) {
|
|
1384
|
+
entry.limit = { context };
|
|
1385
|
+
}
|
|
1386
|
+
models[model.namespaced] = entry;
|
|
1387
|
+
}
|
|
1388
|
+
return {
|
|
1389
|
+
provider: {
|
|
1390
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
1391
|
+
name: "OpenCodex",
|
|
1392
|
+
kind: "openai-compatible",
|
|
1393
|
+
enabled: true,
|
|
1394
|
+
source: "custom",
|
|
1395
|
+
options: {
|
|
1396
|
+
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
1397
|
+
baseURL: ctx.baseUrl.replace(/\/v1\/?$/, "") + "/v1",
|
|
1398
|
+
apiKeyRequired: true,
|
|
1399
|
+
},
|
|
343
1400
|
models,
|
|
344
1401
|
},
|
|
345
1402
|
},
|
|
346
1403
|
};
|
|
347
1404
|
}
|
|
348
1405
|
|
|
1406
|
+
export interface ClineProviderSettings {
|
|
1407
|
+
provider: string;
|
|
1408
|
+
baseUrl: string;
|
|
1409
|
+
apiKey?: string;
|
|
1410
|
+
model?: string;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
export interface ClineProviderBlock {
|
|
1414
|
+
settings: ClineProviderSettings;
|
|
1415
|
+
tokenSource?: string;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
export interface ClineGeneratedConfig {
|
|
1419
|
+
version: number;
|
|
1420
|
+
providers: Record<string, ClineProviderBlock>;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function buildClineClientConfig(ctx: ExportContext): ClineGeneratedConfig {
|
|
1424
|
+
const models = normalizeExportModels(ctx.models);
|
|
1425
|
+
const defaultModel = models[0]?.namespaced ?? "opencodex";
|
|
1426
|
+
return {
|
|
1427
|
+
version: 1,
|
|
1428
|
+
providers: {
|
|
1429
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
1430
|
+
settings: {
|
|
1431
|
+
provider: "openai-compatible",
|
|
1432
|
+
baseUrl: ctx.baseUrl.replace(/\/v1\/?$/, "") + "/v1",
|
|
1433
|
+
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
1434
|
+
model: defaultModel,
|
|
1435
|
+
},
|
|
1436
|
+
tokenSource: "manual",
|
|
1437
|
+
},
|
|
1438
|
+
},
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Per-client model counts, read back off the SERIALIZED document rather than
|
|
1444
|
+
* recomputed from the input rows: `modelsWithoutLimits` drives a GUI line about
|
|
1445
|
+
* the bytes the user actually receives, so a parallel reimplementation of the
|
|
1446
|
+
* "authoritative context window" rule would be free to drift from it.
|
|
1447
|
+
*/
|
|
1448
|
+
function summarizeOpencode(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1449
|
+
const models = Object.values((document as OpencodeGeneratedConfig | undefined)?.provider?.[OPENCODE_PROVIDER_ID]?.models ?? {});
|
|
1450
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length };
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function summarizeCline(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1454
|
+
const provider = (document as ClineGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID];
|
|
1455
|
+
return { modelCount: provider ? 1 : 0, modelsWithoutLimits: 0 };
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function summarizePi(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1459
|
+
const models = (document as PiGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
|
|
1460
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function summarizeOmp(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1464
|
+
const models = (document as OmpGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
|
|
1465
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function summarizeHermes(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1469
|
+
const models = (document as HermesGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
|
|
1470
|
+
// Hermes carries selectors only; it has no per-model limit to be missing.
|
|
1471
|
+
return { modelCount: models.length, modelsWithoutLimits: 0 };
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function summarizeOpenclaw(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1475
|
+
const models = (document as OpenclawGeneratedConfig | undefined)?.models?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
|
|
1476
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function summarizeKimi(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1480
|
+
const models = Object.values((document as KimiGeneratedConfig | undefined)?.models ?? {});
|
|
1481
|
+
// A model with no authoritative window is omitted entirely, so every model
|
|
1482
|
+
// present carries max_context_size by construction.
|
|
1483
|
+
return { modelCount: models.length, modelsWithoutLimits: 0 };
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
function summarizeGajae(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1487
|
+
const models = (document as GajaeGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
|
|
1488
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function summarizeDsh(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1492
|
+
const models = (document as DshGeneratedConfig | undefined)?.["llm-pi-ai"]?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
|
|
1493
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function summarizeMcode(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1497
|
+
const models = Object.values((document as McodeGeneratedConfig | undefined)?.custom_provider?.[OPENCODE_PROVIDER_ID]?.models ?? {});
|
|
1498
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length };
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function summarizeZcode(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1502
|
+
const models = Object.values((document as ZcodeGeneratedConfig | undefined)?.provider?.[OPENCODE_PROVIDER_ID]?.models ?? {});
|
|
1503
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length };
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/** One fragment at `path`, built from this client's own document. */
|
|
1507
|
+
function singleFragment(clientId: ExportClientId, path: readonly string[], value: unknown): ManagedContribution {
|
|
1508
|
+
return { clientId, fragments: [{ path, value }] };
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function buildOpencodeContribution(ctx: ExportContext): ManagedContribution {
|
|
1512
|
+
const doc = buildOpencodeClientConfig(ctx);
|
|
1513
|
+
return singleFragment("opencode", ["provider", OPENCODE_PROVIDER_ID], doc.provider[OPENCODE_PROVIDER_ID]);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function buildPiContribution(ctx: ExportContext): ManagedContribution {
|
|
1517
|
+
const doc = buildPiClientConfig(ctx);
|
|
1518
|
+
return singleFragment("pi", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function buildOmpContribution(ctx: ExportContext): ManagedContribution {
|
|
1522
|
+
const doc = buildOmpClientConfig(ctx);
|
|
1523
|
+
return singleFragment("omp", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function buildHermesContribution(ctx: ExportContext): ManagedContribution {
|
|
1527
|
+
const doc = buildHermesClientConfig(ctx);
|
|
1528
|
+
return singleFragment("hermes", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function buildOpenclawContribution(ctx: ExportContext): ManagedContribution {
|
|
1532
|
+
const doc = buildOpenclawClientConfig(ctx);
|
|
1533
|
+
return singleFragment("openclaw", ["models", "providers", OPENCODE_PROVIDER_ID], doc.models.providers[OPENCODE_PROVIDER_ID]);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Kimi is why a contribution is a LIST: it owns the provider block AND one
|
|
1538
|
+
* `models` entry per model. A writer that only knew about the provider would
|
|
1539
|
+
* strand every model entry on disable.
|
|
1540
|
+
*/
|
|
1541
|
+
function buildKimiContribution(ctx: ExportContext): ManagedContribution {
|
|
1542
|
+
const doc = buildKimiClientConfig(ctx);
|
|
1543
|
+
const fragments: ManagedFragment[] = [
|
|
1544
|
+
{ path: ["providers", OPENCODE_PROVIDER_ID], value: doc.providers[OPENCODE_PROVIDER_ID] },
|
|
1545
|
+
];
|
|
1546
|
+
for (const [alias, block] of Object.entries(doc.models)) {
|
|
1547
|
+
fragments.push({ path: ["models", alias], value: block });
|
|
1548
|
+
}
|
|
1549
|
+
return { clientId: "kimi", fragments };
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function buildGajaeContribution(ctx: ExportContext): ManagedContribution {
|
|
1553
|
+
const doc = buildGajaeClientConfig(ctx);
|
|
1554
|
+
return singleFragment("gajae", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function buildDshContribution(ctx: ExportContext): ManagedContribution {
|
|
1558
|
+
const doc = buildDshClientConfig(ctx);
|
|
1559
|
+
return singleFragment("dsh", ["llm-pi-ai", "providers", OPENCODE_PROVIDER_ID], doc["llm-pi-ai"].providers[OPENCODE_PROVIDER_ID]);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function buildMcodeContribution(ctx: ExportContext): ManagedContribution {
|
|
1563
|
+
const doc = buildMcodeClientConfig(ctx);
|
|
1564
|
+
return singleFragment("mcode", ["custom_provider", OPENCODE_PROVIDER_ID], doc.custom_provider[OPENCODE_PROVIDER_ID]);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function buildZcodeContribution(ctx: ExportContext): ManagedContribution {
|
|
1568
|
+
const doc = buildZcodeClientConfig(ctx);
|
|
1569
|
+
return singleFragment("zcode", ["provider", OPENCODE_PROVIDER_ID], doc.provider[OPENCODE_PROVIDER_ID]);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Prime Agent (PrimeIntellect) is the pi coding agent shipped under a different
|
|
1574
|
+
* brand rather than a lookalike: its package declares a `piConfig` block, and
|
|
1575
|
+
* the agent derives its config directory (`.prime/agent`) and env prefix from
|
|
1576
|
+
* that block alone. `models.json` is therefore the SAME contract Pi reads, so
|
|
1577
|
+
* this client reuses Pi's builder and summarizer verbatim. Restating the shape
|
|
1578
|
+
* here would create a second copy of one fact, which is exactly how the
|
|
1579
|
+
* "anything that is not OpenCode must be Pi" summarizer bug happened.
|
|
1580
|
+
*
|
|
1581
|
+
* The one thing that could still differ is the path we own, and it does not:
|
|
1582
|
+
* Prime keeps our entries under the same `providers.<id>` key. So the only new
|
|
1583
|
+
* code is stamping the right client id on the ownership record.
|
|
1584
|
+
*/
|
|
1585
|
+
function buildPrimeContribution(ctx: ExportContext): ManagedContribution {
|
|
1586
|
+
const doc = buildPiClientConfig(ctx);
|
|
1587
|
+
return singleFragment("prime", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function buildClineContribution(ctx: ExportContext): ManagedContribution {
|
|
1591
|
+
const doc = buildClineClientConfig(ctx);
|
|
1592
|
+
return singleFragment("cline", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
349
1595
|
export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
350
1596
|
opencode: {
|
|
351
1597
|
id: "opencode",
|
|
@@ -354,14 +1600,162 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
354
1600
|
apiKeyEnv: OPENCODE_API_KEY_ENV,
|
|
355
1601
|
exportHint: `export ${OPENCODE_API_KEY_ENV}=<your key>`,
|
|
356
1602
|
build: buildOpencodeClientConfig,
|
|
1603
|
+
format: "json",
|
|
1604
|
+
summarize: summarizeOpencode,
|
|
1605
|
+
buildContribution: buildOpencodeContribution,
|
|
1606
|
+
// carries the dedicated header in provider options
|
|
1607
|
+
loopbackOnly: false,
|
|
357
1608
|
},
|
|
358
1609
|
pi: {
|
|
359
1610
|
id: "pi",
|
|
360
1611
|
filename: "pi-models.json",
|
|
361
|
-
destination:
|
|
362
|
-
apiKeyEnv:
|
|
363
|
-
exportHint:
|
|
1612
|
+
destination: env => piConfigPath(env),
|
|
1613
|
+
apiKeyEnv: "",
|
|
1614
|
+
exportHint: "Pi reads a non-secret placeholder from models.json; loopback needs no key.",
|
|
364
1615
|
build: buildPiClientConfig,
|
|
1616
|
+
format: "json",
|
|
1617
|
+
summarize: summarizePi,
|
|
1618
|
+
buildContribution: buildPiContribution,
|
|
1619
|
+
// No header field in Pi's provider block, so there is nowhere to put the
|
|
1620
|
+
// dedicated admission header a remote bind requires.
|
|
1621
|
+
loopbackOnly: true,
|
|
1622
|
+
},
|
|
1623
|
+
omp: {
|
|
1624
|
+
id: "omp",
|
|
1625
|
+
filename: "omp-models.yaml",
|
|
1626
|
+
destination: env => ompModelsConfigPath(env),
|
|
1627
|
+
apiKeyEnv: "",
|
|
1628
|
+
exportHint: "OMP reads a non-secret placeholder from models.yml; loopback needs no key.",
|
|
1629
|
+
build: buildOmpClientConfig,
|
|
1630
|
+
format: "yaml",
|
|
1631
|
+
summarize: summarizeOmp,
|
|
1632
|
+
buildContribution: buildOmpContribution,
|
|
1633
|
+
// OMP supports provider-level headers, but remote credential wiring is
|
|
1634
|
+
// intentionally deferred from this initial loopback-only integration.
|
|
1635
|
+
loopbackOnly: true,
|
|
1636
|
+
},
|
|
1637
|
+
hermes: {
|
|
1638
|
+
id: "hermes",
|
|
1639
|
+
filename: "hermes-config.yaml",
|
|
1640
|
+
destination: env => hermesConfigPath(env),
|
|
1641
|
+
apiKeyEnv: HERMES_API_KEY_ENV,
|
|
1642
|
+
exportHint: `export ${HERMES_API_KEY_ENV}=<your key>`,
|
|
1643
|
+
build: buildHermesClientConfig,
|
|
1644
|
+
format: "yaml",
|
|
1645
|
+
summarize: summarizeHermes,
|
|
1646
|
+
buildContribution: buildHermesContribution,
|
|
1647
|
+
// extra_headers carries the dedicated header
|
|
1648
|
+
loopbackOnly: false,
|
|
1649
|
+
},
|
|
1650
|
+
openclaw: {
|
|
1651
|
+
id: "openclaw",
|
|
1652
|
+
filename: "openclaw.json5",
|
|
1653
|
+
destination: env => openclawConfigPath(env),
|
|
1654
|
+
apiKeyEnv: OPENCLAW_API_KEY_ENV,
|
|
1655
|
+
exportHint: `export ${OPENCLAW_API_KEY_ENV}=<your key>`,
|
|
1656
|
+
build: buildOpenclawClientConfig,
|
|
1657
|
+
format: "json5",
|
|
1658
|
+
summarize: summarizeOpenclaw,
|
|
1659
|
+
buildContribution: buildOpenclawContribution,
|
|
1660
|
+
// headers carries the dedicated header
|
|
1661
|
+
loopbackOnly: false,
|
|
1662
|
+
},
|
|
1663
|
+
kimi: {
|
|
1664
|
+
id: "kimi",
|
|
1665
|
+
filename: "kimi-config.toml",
|
|
1666
|
+
destination: env => kimiConfigPath(env),
|
|
1667
|
+
// Kimi reads credentials only from its own file, so there is no env var to
|
|
1668
|
+
// export: a loopback bind uses the placeholder, and a remote bind is
|
|
1669
|
+
// refused rather than handed the user's real key.
|
|
1670
|
+
apiKeyEnv: "",
|
|
1671
|
+
exportHint: "Kimi Code reads credentials from its config file; loopback needs no key.",
|
|
1672
|
+
build: buildKimiClientConfig,
|
|
1673
|
+
format: "toml",
|
|
1674
|
+
summarize: summarizeKimi,
|
|
1675
|
+
buildContribution: buildKimiContribution,
|
|
1676
|
+
// no header field, and credentials come only from this file
|
|
1677
|
+
loopbackOnly: true,
|
|
1678
|
+
},
|
|
1679
|
+
gajae: {
|
|
1680
|
+
id: "gajae",
|
|
1681
|
+
filename: "gajae-models.yaml",
|
|
1682
|
+
destination: env => gajaeConfigPath(env),
|
|
1683
|
+
apiKeyEnv: GAJAE_API_KEY_ENV,
|
|
1684
|
+
exportHint: `export ${GAJAE_API_KEY_ENV}=<your key>`,
|
|
1685
|
+
build: buildGajaeClientConfig,
|
|
1686
|
+
format: "yaml",
|
|
1687
|
+
summarize: summarizeGajae,
|
|
1688
|
+
buildContribution: buildGajaeContribution,
|
|
1689
|
+
// strict schema with no header field, so the dedicated header has nowhere to go
|
|
1690
|
+
loopbackOnly: true,
|
|
1691
|
+
},
|
|
1692
|
+
dsh: {
|
|
1693
|
+
id: "dsh",
|
|
1694
|
+
filename: "settings.yaml",
|
|
1695
|
+
destination: env => dshConfigPath(env),
|
|
1696
|
+
apiKeyEnv: "",
|
|
1697
|
+
exportHint: "DSH uses a non-secret loopback bearer placeholder in settings.yaml; loopback needs no key.",
|
|
1698
|
+
build: buildDshClientConfig,
|
|
1699
|
+
format: "yaml",
|
|
1700
|
+
summarize: summarizeDsh,
|
|
1701
|
+
buildContribution: buildDshContribution,
|
|
1702
|
+
loopbackOnly: true,
|
|
1703
|
+
},
|
|
1704
|
+
mcode: {
|
|
1705
|
+
id: "mcode",
|
|
1706
|
+
filename: "mcode-config.yaml",
|
|
1707
|
+
destination: env => mcodeConfigPath(env),
|
|
1708
|
+
apiKeyEnv: "",
|
|
1709
|
+
exportHint: "MiniMax Code reads a non-secret placeholder from config.yaml; loopback needs no key.",
|
|
1710
|
+
build: buildMcodeClientConfig,
|
|
1711
|
+
format: "yaml",
|
|
1712
|
+
summarize: summarizeMcode,
|
|
1713
|
+
buildContribution: buildMcodeContribution,
|
|
1714
|
+
// MCode persists this credential and exposes no dedicated proxy-admission
|
|
1715
|
+
// header field, so real keys are never serialized and remote binds refuse.
|
|
1716
|
+
loopbackOnly: true,
|
|
1717
|
+
},
|
|
1718
|
+
zcode: {
|
|
1719
|
+
id: "zcode",
|
|
1720
|
+
filename: "config.json",
|
|
1721
|
+
destination: env => zcodeConfigPath(env),
|
|
1722
|
+
apiKeyEnv: "",
|
|
1723
|
+
exportHint: "ZCode reads a non-secret placeholder from v2/config.json; loopback needs no key.",
|
|
1724
|
+
build: buildZcodeClientConfig,
|
|
1725
|
+
format: "json",
|
|
1726
|
+
summarize: summarizeZcode,
|
|
1727
|
+
buildContribution: buildZcodeContribution,
|
|
1728
|
+
// ZCode persists the credential in its own file and has no dedicated
|
|
1729
|
+
// proxy-admission header field, so real keys are never serialized and
|
|
1730
|
+
// remote binds refuse — same reasoning as MCode.
|
|
1731
|
+
loopbackOnly: true,
|
|
1732
|
+
},
|
|
1733
|
+
prime: {
|
|
1734
|
+
id: "prime",
|
|
1735
|
+
filename: "prime-models.json",
|
|
1736
|
+
destination: env => primeConfigPath(env),
|
|
1737
|
+
apiKeyEnv: "",
|
|
1738
|
+
exportHint: "Prime Agent reads a non-secret placeholder from models.json; loopback needs no key.",
|
|
1739
|
+
build: buildPiClientConfig,
|
|
1740
|
+
format: "json",
|
|
1741
|
+
summarize: summarizePi,
|
|
1742
|
+
buildContribution: buildPrimeContribution,
|
|
1743
|
+
// Prime's provider block does accept `headers`, so a dedicated admission
|
|
1744
|
+
// header has somewhere to live, but remote credential wiring is deferred
|
|
1745
|
+
// from this initial loopback-only integration — same stance as OMP's.
|
|
1746
|
+
loopbackOnly: true,
|
|
1747
|
+
},
|
|
1748
|
+
cline: {
|
|
1749
|
+
id: "cline",
|
|
1750
|
+
filename: "providers.json",
|
|
1751
|
+
destination: env => clineConfigPath(env),
|
|
1752
|
+
apiKeyEnv: "",
|
|
1753
|
+
exportHint: "Cline reads a non-secret placeholder from providers.json; loopback needs no key.",
|
|
1754
|
+
build: buildClineClientConfig,
|
|
1755
|
+
format: "json",
|
|
1756
|
+
summarize: summarizeCline,
|
|
1757
|
+
buildContribution: buildClineContribution,
|
|
1758
|
+
loopbackOnly: true,
|
|
365
1759
|
},
|
|
366
1760
|
};
|
|
367
1761
|
|
|
@@ -375,3 +1769,28 @@ export function isExportClientId(value: string): value is ExportClientId {
|
|
|
375
1769
|
export function buildClientConfig(client: ExportClientId, ctx: ExportContext): unknown {
|
|
376
1770
|
return EXPORT_CLIENTS[client].build(ctx);
|
|
377
1771
|
}
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* The bytes a user actually receives, plus what they are. One place turns a
|
|
1775
|
+
* client id into text so the CLI, the API and the GUI cannot disagree about
|
|
1776
|
+
* format or media type — and so no consumer has to infer either from a
|
|
1777
|
+
* filename.
|
|
1778
|
+
*/
|
|
1779
|
+
export function buildClientConfigText(
|
|
1780
|
+
client: ExportClientId,
|
|
1781
|
+
ctx: ExportContext,
|
|
1782
|
+
): { document: unknown; text: string; format: ConfigFormat; mediaType: string } {
|
|
1783
|
+
const spec = EXPORT_CLIENTS[client];
|
|
1784
|
+
const document = spec.build(ctx);
|
|
1785
|
+
return {
|
|
1786
|
+
document,
|
|
1787
|
+
text: serializeDocument(document, spec.format),
|
|
1788
|
+
format: spec.format,
|
|
1789
|
+
mediaType: FORMAT_MEDIA_TYPE[spec.format],
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/** The fragments opencodex owns in a client's config (writer-side). */
|
|
1794
|
+
export function buildClientContribution(client: ExportClientId, ctx: ExportContext): ManagedContribution {
|
|
1795
|
+
return EXPORT_CLIENTS[client].buildContribution(ctx);
|
|
1796
|
+
}
|