@bitkyc08/opencodex 2.42.0 → 2.43.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 +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Shared client export constants.
|
|
2
|
+
import type { OcxConfig } from "../../types";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/** Provider key owned by this project; the only key any exporter ever emits. */
|
|
6
|
+
export const OPENCODE_PROVIDER_ID = "opencodex";
|
|
7
|
+
|
|
8
|
+
export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Env var carrying the proxy admission key to opencode. The config only ever holds the
|
|
12
|
+
* `{env:...}` reference, so the secret never lands on disk. opencode substitutes it at
|
|
13
|
+
* load time.
|
|
14
|
+
*/
|
|
15
|
+
export const OPENCODE_API_KEY_ENV = "OPENCODEX_OPENCODE_API_KEY";
|
|
16
|
+
|
|
17
|
+
/** Env reference shared by apiKey and the dedicated proxy admission header. */
|
|
18
|
+
export const OPENCODE_API_KEY_ENV_REF = `{env:${OPENCODE_API_KEY_ENV}}`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hermes interpolates `${VAR}` anywhere in config.yaml, so the credential stays
|
|
22
|
+
* in the environment exactly as it does for OpenCode.
|
|
23
|
+
*/
|
|
24
|
+
export const HERMES_API_KEY_ENV = "OPENCODEX_HERMES_API_KEY";
|
|
25
|
+
export const HERMES_API_KEY_ENV_REF = `\${${HERMES_API_KEY_ENV}}`;
|
|
26
|
+
|
|
27
|
+
/** OpenClaw interpolates `${UPPERCASE_VAR}` and fails closed when it is unset. */
|
|
28
|
+
export const OPENCLAW_API_KEY_ENV = "OPENCODEX_OPENCLAW_API_KEY";
|
|
29
|
+
export const OPENCLAW_API_KEY_ENV_REF = `\${${OPENCLAW_API_KEY_ENV}}`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Placeholder credential for loopback-only clients (Kimi, Pi). A loopback
|
|
33
|
+
* bind needs no real admission key, so we emit the same placeholder the Grok
|
|
34
|
+
* managed block uses rather than a user secret. Pi resolves `apiKey` before
|
|
35
|
+
* building its model list and hides the provider when an env reference is unset.
|
|
36
|
+
*/
|
|
37
|
+
export const LOOPBACK_API_KEY_PLACEHOLDER = "opencodex-loopback";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gajae's `apiKeyEnv` is env-name-only and fail-closed. Its sibling `apiKey`
|
|
41
|
+
* falls back to treating the literal text as the token when the variable is
|
|
42
|
+
* unset, which would silently ship a bogus credential — so we never emit it.
|
|
43
|
+
*/
|
|
44
|
+
export const GAJAE_API_KEY_ENV = "OPENCODEX_GAJAE_API_KEY";
|
|
45
|
+
|
|
46
|
+
/** Pi's wire-dialect selector for an OpenAI-compatible endpoint. */
|
|
47
|
+
export const PI_API_DIALECT = "openai-completions";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* opencode's config schema rejects a `limit` block that carries `context` without
|
|
51
|
+
* `output`, but CatalogModel has no authoritative per-model output field. Dropping
|
|
52
|
+
* `limit` entirely would also throw away the authoritative context window we DO have,
|
|
53
|
+
* so the block is emitted with this budget standing in for the missing half.
|
|
54
|
+
*
|
|
55
|
+
* The value matches REASONING_MAX_TOKENS_CEILING in src/adapters/anthropic.ts — the
|
|
56
|
+
* project's existing "safe ceiling across current models" figure. It is a ceiling for
|
|
57
|
+
* schema validity, NOT a claim about any specific model's true maximum, and it is
|
|
58
|
+
* clamped to the context window so a small-context model can never be emitted with
|
|
59
|
+
* output > context. Pi's `maxTokens` uses the same stand-in and the same clamp.
|
|
60
|
+
*/
|
|
61
|
+
export const SCHEMA_REQUIRED_OUTPUT_BUDGET = 32_000;
|
|
62
|
+
|
|
63
|
+
/** Deterministic loopback default for exported provider-block helpers in tests. */
|
|
64
|
+
export const OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG: OcxConfig = {
|
|
65
|
+
port: 10100,
|
|
66
|
+
hostname: "127.0.0.1",
|
|
67
|
+
defaultProvider: "mock",
|
|
68
|
+
providers: { mock: { adapter: "openai-chat", baseUrl: "http://127.0.0.1/v1" } },
|
|
69
|
+
} as OcxConfig;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Shared client export contracts.
|
|
2
|
+
import type { OcxConfig } from "../../types";
|
|
3
|
+
import type { ConfigFormat } from "../../integrations/serialize";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One entry opencodex owns inside a client's config: the JSON path to it and
|
|
7
|
+
* the value we put there.
|
|
8
|
+
*
|
|
9
|
+
* A path list rather than a single provider key because ownership is not
|
|
10
|
+
* always one entry — Kimi owns its provider block AND one model entry per
|
|
11
|
+
* model, and a writer that only knew about the provider would strand the rest
|
|
12
|
+
* (devlog 260802 006 §2).
|
|
13
|
+
*/
|
|
14
|
+
export interface ManagedFragment {
|
|
15
|
+
path: readonly string[];
|
|
16
|
+
value: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Everything opencodex contributes to one client's config, as one unit. */
|
|
20
|
+
export interface ManagedContribution {
|
|
21
|
+
clientId: ExportClientId;
|
|
22
|
+
fragments: readonly ManagedFragment[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type BuildContribution = (ctx: ExportContext) => ManagedContribution;
|
|
26
|
+
|
|
27
|
+
export interface OpencodeLaunchEnv {
|
|
28
|
+
[key: string]: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Visible catalog entry keyed by the proxy's canonical namespaced selector. */
|
|
32
|
+
export interface OpencodeCatalogModel {
|
|
33
|
+
namespaced: string;
|
|
34
|
+
/** Hub-resolved Fast availability. Missing metadata means unavailable. */
|
|
35
|
+
fastRowAvailable?: boolean;
|
|
36
|
+
native?: boolean;
|
|
37
|
+
provider?: string;
|
|
38
|
+
id?: string;
|
|
39
|
+
contextWindow?: number;
|
|
40
|
+
displayName?: string;
|
|
41
|
+
/** Declared effort ladder. Exported as opencode model variants where the client reads them. */
|
|
42
|
+
reasoningEfforts?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Declared default effort. Carried so every client export reads one deduped, visibility-
|
|
45
|
+
* filtered ladder per model. The opencode serializer deliberately does NOT turn it into a
|
|
46
|
+
* model-level setting — see {@link opencodeEffortVariants} for why.
|
|
47
|
+
*/
|
|
48
|
+
defaultReasoningEffort?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One proxy-routed model destined for a client config. Deliberately narrower than
|
|
53
|
+
* `CatalogModel` so a serializer cannot reach for a field that does not survive the
|
|
54
|
+
* `/api/models` boundary.
|
|
55
|
+
*/
|
|
56
|
+
export interface ExportModel {
|
|
57
|
+
/** Canonical proxy selector: `provider/id`, or bare slug for native. */
|
|
58
|
+
namespaced: string;
|
|
59
|
+
/** Hub-resolved Fast availability; exporters never infer it from local config. */
|
|
60
|
+
fastRowAvailable?: boolean;
|
|
61
|
+
provider: string;
|
|
62
|
+
id: string;
|
|
63
|
+
/** Native OpenAI entry. Read by the shared label rule. */
|
|
64
|
+
native?: boolean;
|
|
65
|
+
displayName?: string;
|
|
66
|
+
contextWindow?: number;
|
|
67
|
+
inputModalities?: string[];
|
|
68
|
+
/** Optional effort ladder exported only to clients that support it. */
|
|
69
|
+
reasoningEfforts?: string[];
|
|
70
|
+
defaultReasoningEffort?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ExportContext {
|
|
74
|
+
/** `http://host:port/v1` — the OpenAI-compatible surface the client dials. */
|
|
75
|
+
baseUrl: string;
|
|
76
|
+
models: readonly ExportModel[];
|
|
77
|
+
/**
|
|
78
|
+
* Live proxy config. Only the OpenCode path reads it: a non-loopback bind moves
|
|
79
|
+
* admission from `apiKey` to the `x-opencodex-api-key` header.
|
|
80
|
+
*/
|
|
81
|
+
config?: OcxConfig;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type ExportClientId =
|
|
85
|
+
| "opencode"
|
|
86
|
+
| "pi"
|
|
87
|
+
| "omp"
|
|
88
|
+
| "hermes"
|
|
89
|
+
| "openclaw"
|
|
90
|
+
| "kimi"
|
|
91
|
+
| "gajae"
|
|
92
|
+
| "dsh"
|
|
93
|
+
| "mcode"
|
|
94
|
+
| "zcode"
|
|
95
|
+
| "prime"
|
|
96
|
+
| "aside";
|
|
97
|
+
|
|
98
|
+
export interface ExportClientSpec {
|
|
99
|
+
id: ExportClientId;
|
|
100
|
+
/** Download filename; matches the destination file's own name (003 §5). */
|
|
101
|
+
filename: string;
|
|
102
|
+
/** Canonical destination for humans. Never written to. */
|
|
103
|
+
destination: (env: NodeJS.ProcessEnv) => string;
|
|
104
|
+
/** Env var the config references; the value is never serialized. */
|
|
105
|
+
apiKeyEnv: string;
|
|
106
|
+
/** Shell line the user runs before launching the client. */
|
|
107
|
+
exportHint: string;
|
|
108
|
+
build: (ctx: ExportContext) => unknown;
|
|
109
|
+
/**
|
|
110
|
+
* Text format of the client's config file. `filename` already carries the
|
|
111
|
+
* extension; this drives serialization and the download media type so no
|
|
112
|
+
* consumer has to infer either from the name.
|
|
113
|
+
*/
|
|
114
|
+
format: ConfigFormat;
|
|
115
|
+
/**
|
|
116
|
+
* Count models in THIS client's document shape. Required so a new client
|
|
117
|
+
* cannot be added without teaching the summarizer about it — the old
|
|
118
|
+
* "anything that is not OpenCode must be Pi" branch was a latent bug.
|
|
119
|
+
*/
|
|
120
|
+
summarize: (document: unknown) => { modelCount: number; modelsWithoutLimits: number };
|
|
121
|
+
/**
|
|
122
|
+
* The fragments opencodex owns inside this client's config. Only the builder
|
|
123
|
+
* knows where a client keeps our entries, so ownership paths originate here
|
|
124
|
+
* rather than being re-derived by the writer.
|
|
125
|
+
*/
|
|
126
|
+
buildContribution: BuildContribution;
|
|
127
|
+
/**
|
|
128
|
+
* True when the generated integration deliberately supports loopback only.
|
|
129
|
+
*
|
|
130
|
+
* `/v1/chat/completions` rejects bearer credentials and requires the
|
|
131
|
+
* dedicated `x-opencodex-api-key` header (AUTH_MATRIX in
|
|
132
|
+
* src/server/auth-cors.ts). If this exporter cannot safely emit that header,
|
|
133
|
+
* it refuses a remote bind rather than generating a config that 401s. Same
|
|
134
|
+
* reasoning as the Grok managed block's non-loopback refusal.
|
|
135
|
+
*/
|
|
136
|
+
loopbackOnly: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface PiModelEntry {
|
|
140
|
+
id: string;
|
|
141
|
+
name: string;
|
|
142
|
+
input: string[];
|
|
143
|
+
contextWindow?: number;
|
|
144
|
+
maxTokens?: number;
|
|
145
|
+
/** Advertised when the catalog row carries a non-empty effort ladder. */
|
|
146
|
+
reasoning?: true;
|
|
147
|
+
/**
|
|
148
|
+
* Constrains pi's own level scale (minimal..max) to the declared ladder: members map to
|
|
149
|
+
* themselves, everything else is hidden (`null`). Without it pi would offer levels the
|
|
150
|
+
* ladder does not contain — harmless for provider-config ladders (the proxy clamps those
|
|
151
|
+
* at the wire) but a real 400 risk for custom-row ladders, which are advertisement-only.
|
|
152
|
+
*/
|
|
153
|
+
thinkingLevelMap?: Record<string, string | null>;
|
|
154
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// DSH config export.
|
|
2
|
+
import type { ExportModel, ExportContext, ManagedContribution } from "./contracts";
|
|
3
|
+
import type { OcxConfig } from "../../types";
|
|
4
|
+
import { providerCodexAccountMode } from "../../providers/registry";
|
|
5
|
+
import { normalizeExportModels, authoritativeContextWindow, exportModelLabel, singleFragment } from "./model-metadata";
|
|
6
|
+
import { OPENCODE_PROVIDER_ID } from "./constants";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/** DSH rc.6 accepts text/image; unknown values degrade to text, while audio-only cannot be represented. */
|
|
10
|
+
function dshInputModalities(modalities: readonly string[] | undefined): string[] | null {
|
|
11
|
+
const declared = modalities ?? [];
|
|
12
|
+
if (declared.length === 0) return ["text"];
|
|
13
|
+
const kept: string[] = [];
|
|
14
|
+
for (const value of declared) {
|
|
15
|
+
if ((value === "text" || value === "image") && !kept.includes(value)) kept.push(value);
|
|
16
|
+
}
|
|
17
|
+
if (kept.length > 0) return kept;
|
|
18
|
+
return declared.every(value => value === "audio") ? null : ["text"];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type DshReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
|
22
|
+
export type DshWireReasoningEffort = DshReasoningEffort | "ultra";
|
|
23
|
+
|
|
24
|
+
export interface DshModelEntry {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
input: string[];
|
|
28
|
+
contextWindow?: number;
|
|
29
|
+
reasoningEfforts?: Partial<Record<DshReasoningEffort, DshWireReasoningEffort>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DshProviderBlock {
|
|
33
|
+
displayName: "OpenCodex";
|
|
34
|
+
api: "openai-responses";
|
|
35
|
+
baseURL: string;
|
|
36
|
+
headers: { Authorization: "Bearer ocx_data_dsh" };
|
|
37
|
+
models: DshModelEntry[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DshGeneratedConfig {
|
|
41
|
+
"llm-pi-ai": {
|
|
42
|
+
providers: Record<string, DshProviderBlock>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DSH_EFFORT_ORDER: readonly DshReasoningEffort[] = ["low", "medium", "high", "xhigh", "max"];
|
|
47
|
+
|
|
48
|
+
function dshReasoningEfforts(model: ExportModel): DshModelEntry["reasoningEfforts"] {
|
|
49
|
+
const offered = new Set<string>();
|
|
50
|
+
for (const raw of model.reasoningEfforts ?? []) {
|
|
51
|
+
const effort = raw.trim().toLowerCase();
|
|
52
|
+
if (effort === "ultra" || DSH_EFFORT_ORDER.includes(effort as DshReasoningEffort)) offered.add(effort);
|
|
53
|
+
}
|
|
54
|
+
if (offered.size === 0) return undefined;
|
|
55
|
+
const entries: Array<[DshReasoningEffort, DshWireReasoningEffort]> = [];
|
|
56
|
+
for (const effort of DSH_EFFORT_ORDER) {
|
|
57
|
+
if (effort !== "max") {
|
|
58
|
+
if (offered.has(effort)) entries.push([effort, effort]);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// DSH's key is the selectable level; the value is what it sends on the
|
|
62
|
+
// wire. Preserve OpenCodex's `ultra` spelling when that is the only
|
|
63
|
+
// highest effort, exactly like the rc.6 `max: ultra` contract.
|
|
64
|
+
if (offered.has("max")) entries.push(["max", "max"]);
|
|
65
|
+
else if (offered.has("ultra")) entries.push(["max", "ultra"]);
|
|
66
|
+
}
|
|
67
|
+
return Object.fromEntries(entries);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isKnownSafeDshCombo(model: ExportModel, config: OcxConfig): boolean {
|
|
71
|
+
const combos = (config as { combos?: unknown }).combos;
|
|
72
|
+
if (typeof combos !== "object" || combos === null || Array.isArray(combos)) return false;
|
|
73
|
+
const combo = (combos as Record<string, unknown>)[model.id];
|
|
74
|
+
if (typeof combo !== "object" || combo === null || Array.isArray(combo)) return false;
|
|
75
|
+
const targets = (combo as { targets?: unknown }).targets;
|
|
76
|
+
if (!Array.isArray(targets) || targets.length === 0) return false;
|
|
77
|
+
return targets.every(target => {
|
|
78
|
+
if (typeof target !== "object" || target === null || Array.isArray(target)) return false;
|
|
79
|
+
const provider = (target as { provider?: unknown }).provider;
|
|
80
|
+
const modelId = (target as { model?: unknown }).model;
|
|
81
|
+
return typeof provider === "string"
|
|
82
|
+
&& provider.length > 0
|
|
83
|
+
&& provider === provider.trim()
|
|
84
|
+
&& provider !== "openai"
|
|
85
|
+
&& typeof modelId === "string"
|
|
86
|
+
&& modelId.length > 0
|
|
87
|
+
&& modelId === modelId.trim();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildDshClientConfig(ctx: ExportContext): DshGeneratedConfig {
|
|
92
|
+
const direct = providerCodexAccountMode("openai", ctx.config?.providers?.openai) === "direct";
|
|
93
|
+
const models: DshModelEntry[] = [];
|
|
94
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
95
|
+
if (direct && (model.native === true || model.provider === "openai")) continue;
|
|
96
|
+
if (direct && model.provider === "combo" && (!ctx.config || !isKnownSafeDshCombo(model, ctx.config))) continue;
|
|
97
|
+
const input = dshInputModalities(model.inputModalities);
|
|
98
|
+
if (input === null) continue;
|
|
99
|
+
const contextWindow = authoritativeContextWindow(model.contextWindow);
|
|
100
|
+
const reasoningEfforts = dshReasoningEfforts(model);
|
|
101
|
+
models.push({
|
|
102
|
+
id: model.namespaced,
|
|
103
|
+
name: exportModelLabel(model),
|
|
104
|
+
input,
|
|
105
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
106
|
+
...(reasoningEfforts ? { reasoningEfforts } : {}),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
"llm-pi-ai": {
|
|
111
|
+
providers: {
|
|
112
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
113
|
+
displayName: "OpenCodex",
|
|
114
|
+
api: "openai-responses",
|
|
115
|
+
baseURL: ctx.baseUrl,
|
|
116
|
+
headers: { Authorization: "Bearer ocx_data_dsh" },
|
|
117
|
+
models,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function summarizeDsh(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
125
|
+
const models = (document as DshGeneratedConfig | undefined)?.["llm-pi-ai"]?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
|
|
126
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function buildDshContribution(ctx: ExportContext): ManagedContribution {
|
|
130
|
+
const doc = buildDshClientConfig(ctx);
|
|
131
|
+
return singleFragment("dsh", ["llm-pi-ai", "providers", OPENCODE_PROVIDER_ID], doc["llm-pi-ai"].providers[OPENCODE_PROVIDER_ID]);
|
|
132
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OpencodeCatalogModel } from "./contracts";
|
|
2
|
+
|
|
3
|
+
const FAST_ROW_SUFFIX = "--fast";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Expand only hub-resolved availability; older hubs without the field advertise no Fast.
|
|
7
|
+
* Reserve every exact input selector before synthesis and retain the first duplicate.
|
|
8
|
+
* Keep routing/capability metadata intact: only the selector, label and availability change.
|
|
9
|
+
*/
|
|
10
|
+
export function expandFastExportModels<T extends OpencodeCatalogModel>(models: readonly T[]): T[] {
|
|
11
|
+
const exact = new Map<string, T>();
|
|
12
|
+
for (const model of models) {
|
|
13
|
+
if (!exact.has(model.namespaced)) exact.set(model.namespaced, model);
|
|
14
|
+
}
|
|
15
|
+
const expanded = [...exact.values()];
|
|
16
|
+
for (const model of exact.values()) {
|
|
17
|
+
if (model.fastRowAvailable !== true) continue;
|
|
18
|
+
const namespaced = `${model.namespaced}${FAST_ROW_SUFFIX}`;
|
|
19
|
+
if (exact.has(namespaced)) continue;
|
|
20
|
+
expanded.push({
|
|
21
|
+
...model,
|
|
22
|
+
namespaced,
|
|
23
|
+
displayName: `${model.displayName || model.id || model.namespaced} Fast`,
|
|
24
|
+
// Re-normalizing cannot synthesize a Fast row from this synthetic row.
|
|
25
|
+
fastRowAvailable: false,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return expanded;
|
|
29
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// MiniMax Code config export.
|
|
2
|
+
import type { ExportContext, ManagedContribution } from "./contracts";
|
|
3
|
+
import { normalizeExportModels, authoritativeContextWindow, singleFragment } from "./model-metadata";
|
|
4
|
+
import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
5
|
+
import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export interface McodeProviderBlock {
|
|
9
|
+
name: "OpenCodex";
|
|
10
|
+
kind: "custom";
|
|
11
|
+
enabled: true;
|
|
12
|
+
api: "anthropic-messages";
|
|
13
|
+
options: {
|
|
14
|
+
apiKey: string;
|
|
15
|
+
baseURL: string;
|
|
16
|
+
authMode: "api-key";
|
|
17
|
+
};
|
|
18
|
+
models: Record<string, McodeModelEntry>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface McodeModelEntry {
|
|
22
|
+
/** MCode uses this value for context accounting and compaction. */
|
|
23
|
+
limit?: { context: number };
|
|
24
|
+
/** MCode exposes these exact levels in `/model` and sends the selected effort. */
|
|
25
|
+
thinking?: { effortOptions: string[] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface McodeGeneratedConfig {
|
|
29
|
+
custom_provider: Record<string, McodeProviderBlock>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* MiniMax Code's `provider add` command persists custom providers under
|
|
34
|
+
* `custom_provider.<id>`. Its current model schema reads `limit.context` for
|
|
35
|
+
* context accounting and `thinking.effortOptions` for the `/model` effort
|
|
36
|
+
* control. Do not emit the removed `thinking.effort` / `defaultEffort` fields:
|
|
37
|
+
* MCode 0.1.6 migrates those into options and keeps the selected effort in the
|
|
38
|
+
* session. Do not emit `defaultModel` either: connecting a client must not
|
|
39
|
+
* silently replace the user's current model selection.
|
|
40
|
+
*/
|
|
41
|
+
export function buildMcodeClientConfig(ctx: ExportContext): McodeGeneratedConfig {
|
|
42
|
+
const models: Record<string, McodeModelEntry> = {};
|
|
43
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
44
|
+
const entry: McodeModelEntry = {};
|
|
45
|
+
const context = authoritativeContextWindow(model.contextWindow);
|
|
46
|
+
if (context !== undefined) entry.limit = { context };
|
|
47
|
+
// `none` is an internal Codex catalog sentinel, not an MCode effort. MCode
|
|
48
|
+
// forwards every option as `output_config.effort` while keeping adaptive
|
|
49
|
+
// thinking enabled, and the Anthropic ingress deliberately accepts only
|
|
50
|
+
// minimal..ultra. Advertising `none` would therefore create a selectable
|
|
51
|
+
// value that cannot disable reasoning and is not forwarded as an effort.
|
|
52
|
+
const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts)
|
|
53
|
+
?.filter(effort => effort !== "none");
|
|
54
|
+
if (efforts && efforts.length > 0) entry.thinking = { effortOptions: efforts };
|
|
55
|
+
models[model.namespaced] = entry;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
custom_provider: {
|
|
59
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
60
|
+
name: "OpenCodex",
|
|
61
|
+
kind: "custom",
|
|
62
|
+
enabled: true,
|
|
63
|
+
api: "anthropic-messages",
|
|
64
|
+
options: {
|
|
65
|
+
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
66
|
+
baseURL: ctx.baseUrl.replace(/\/v1\/?$/, ""),
|
|
67
|
+
authMode: "api-key",
|
|
68
|
+
},
|
|
69
|
+
models,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function summarizeMcode(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
76
|
+
const models = Object.values((document as McodeGeneratedConfig | undefined)?.custom_provider?.[OPENCODE_PROVIDER_ID]?.models ?? {});
|
|
77
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildMcodeContribution(ctx: ExportContext): ManagedContribution {
|
|
81
|
+
const doc = buildMcodeClientConfig(ctx);
|
|
82
|
+
return singleFragment("mcode", ["custom_provider", OPENCODE_PROVIDER_ID], doc.custom_provider[OPENCODE_PROVIDER_ID]);
|
|
83
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Shared client export model metadata.
|
|
2
|
+
import { SCHEMA_REQUIRED_OUTPUT_BUDGET } from "./constants";
|
|
3
|
+
import { expandFastExportModels } from "./fast-models";
|
|
4
|
+
import type { OpencodeCatalogModel, ExportModel, ExportClientId, ManagedContribution } from "./contracts";
|
|
5
|
+
import type { OcxConfig } from "../../types";
|
|
6
|
+
import { shouldInjectApiAuthHeader } from "../../codex/inject";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Authoritative context window, or undefined. Never guesses: a missing, non-finite, or
|
|
11
|
+
* non-positive value means the serializer omits every context-derived field.
|
|
12
|
+
*/
|
|
13
|
+
export function authoritativeContextWindow(contextWindow: number | undefined): number | undefined {
|
|
14
|
+
if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
|
|
15
|
+
const integer = Math.floor(contextWindow);
|
|
16
|
+
return integer > 0 ? integer : undefined;
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Schema-required output budget for a known context window. */
|
|
22
|
+
export function outputBudgetFor(context: number): number {
|
|
23
|
+
return Math.min(SCHEMA_REQUIRED_OUTPUT_BUDGET, context);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Modalities a given client's schema will actually accept.
|
|
28
|
+
*
|
|
29
|
+
* Our internal vocabulary is `text | image | audio` (ALLOWED_INPUT_MODALITIES in
|
|
30
|
+
* src/server/management/model-routes.ts). Pi and Gajae accept only
|
|
31
|
+
* `text | image`, and both reject the WHOLE config file over one out-of-enum
|
|
32
|
+
* value — Gajae reports `/providers/opencodex/models/N/input/2: Invalid option`
|
|
33
|
+
* and falls back to its built-in list, Pi returns an empty model config. So a
|
|
34
|
+
* single `audio` model takes every routed model down with it. That is not
|
|
35
|
+
* hypothetical: zenmux/meta-muse-spark-1.1 advertises audio and did exactly
|
|
36
|
+
* this. It is also the same defect the Codex catalog had with `video`, where
|
|
37
|
+
* the app showed zero apps (tests/codex-integration/catalog-input-modality-enum.test.ts).
|
|
38
|
+
*
|
|
39
|
+
* UNKNOWN and INCOMPATIBLE are different inputs, and the Codex fix could
|
|
40
|
+
* conflate them safely only because its enum is wider. A model with nothing
|
|
41
|
+
* declared is unknown, and `text` is the honest floor — every routed model takes
|
|
42
|
+
* prompts. A model declaring `["audio"]` and nothing else is incompatible with a
|
|
43
|
+
* text|image client, and rewriting it to `["text"]` would advertise a capability
|
|
44
|
+
* it does not have. That input is reachable three ways: `ocx models add
|
|
45
|
+
* --modalities audio`, `/api/custom-models`, and provider discovery.
|
|
46
|
+
*
|
|
47
|
+
* So unknown falls back to text and incompatible returns null, which drops the
|
|
48
|
+
* row. Omitting a model costs the user a line in a picker; fabricating `text`
|
|
49
|
+
* costs them a model that fails at call time with no explanation.
|
|
50
|
+
*
|
|
51
|
+
* Deliberately NOT applied in `ExportModel` construction: the management and CLI
|
|
52
|
+
* boundaries carry catalog modalities verbatim on purpose, and stripping `audio`
|
|
53
|
+
* globally would destroy valid metadata before the destination is known.
|
|
54
|
+
*/
|
|
55
|
+
const CLIENT_INPUT_MODALITIES: Record<"pi" | "gajae", ReadonlySet<string>> = {
|
|
56
|
+
pi: new Set(["text", "image"]),
|
|
57
|
+
gajae: new Set(["text", "image"]),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** `null` means the model cannot be represented for this client — drop the row. */
|
|
61
|
+
export function inputModalitiesForClient(
|
|
62
|
+
client: "pi" | "gajae",
|
|
63
|
+
modalities: readonly string[] | undefined,
|
|
64
|
+
): string[] | null {
|
|
65
|
+
const declared = modalities ?? [];
|
|
66
|
+
if (declared.length === 0) return ["text"];
|
|
67
|
+
const accepted = CLIENT_INPUT_MODALITIES[client];
|
|
68
|
+
const kept: string[] = [];
|
|
69
|
+
for (const value of declared) {
|
|
70
|
+
if (accepted.has(value) && !kept.includes(value)) kept.push(value);
|
|
71
|
+
}
|
|
72
|
+
return kept.length > 0 ? kept : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Label shared by every client: `"<displayName|id> (<native|provider|routed>)"`. The
|
|
77
|
+
* provider suffix is what makes two same-named models from different upstreams
|
|
78
|
+
* distinguishable in a client's model picker.
|
|
79
|
+
*/
|
|
80
|
+
export function exportModelLabel(model: OpencodeCatalogModel): string {
|
|
81
|
+
const providerLabel = model.native ? "native" : (model.provider ?? "routed");
|
|
82
|
+
const id = model.id ?? model.namespaced;
|
|
83
|
+
if (model.displayName && model.displayName.length > 0) {
|
|
84
|
+
return `${model.displayName} (${providerLabel})`;
|
|
85
|
+
}
|
|
86
|
+
return `${id} (${providerLabel})`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Shared precondition for every serializer: expand hub-approved Fast rows, drop duplicate
|
|
91
|
+
* `namespaced` (first wins, native rows lead `/api/models`) and sort so calls with the same
|
|
92
|
+
* models produce identical bytes. Stability matters because the GUI shows a diffable
|
|
93
|
+
* preview and agents may checksum the payload.
|
|
94
|
+
*/
|
|
95
|
+
export function normalizeExportModels(models: readonly ExportModel[]): ExportModel[] {
|
|
96
|
+
return expandFastExportModels(models)
|
|
97
|
+
.sort((a, b) => (a.namespaced < b.namespaced ? -1 : a.namespaced > b.namespaced ? 1 : 0));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Extra headers a non-loopback bind needs, or nothing on loopback. */
|
|
101
|
+
export function proxyAdmissionHeaders(config: OcxConfig | undefined, envRef: string): Record<string, string> | undefined {
|
|
102
|
+
return shouldInjectApiAuthHeader(config) ? { "x-opencodex-api-key": envRef } : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** One fragment at `path`, built from this client's own document. */
|
|
106
|
+
export function singleFragment(clientId: ExportClientId, path: readonly string[], value: unknown): ManagedContribution {
|
|
107
|
+
return { clientId, fragments: [{ path, value }] };
|
|
108
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Oh My Pi config export.
|
|
2
|
+
import type { PiModelEntry, ExportModel, ExportContext, ManagedContribution } from "./contracts";
|
|
3
|
+
import { PI_API_DIALECT, OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
|
|
4
|
+
import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, outputBudgetFor, singleFragment } from "./model-metadata";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* omp accepts a model-level API override. Keep the provider on Chat
|
|
9
|
+
* Completions so routed providers retain their established wire format, while
|
|
10
|
+
* native OpenAI models can use the lossless Responses surface.
|
|
11
|
+
*/
|
|
12
|
+
export interface OmpModelEntry extends PiModelEntry {
|
|
13
|
+
api?: "openai-responses";
|
|
14
|
+
/** omp requires this flag before it honors a thinking block. */
|
|
15
|
+
reasoning?: true;
|
|
16
|
+
thinking?: {
|
|
17
|
+
mode: "effort";
|
|
18
|
+
efforts: string[];
|
|
19
|
+
defaultLevel?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OmpProviderBlock {
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
api: typeof PI_API_DIALECT;
|
|
26
|
+
apiKey: string;
|
|
27
|
+
models: OmpModelEntry[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OmpGeneratedConfig {
|
|
31
|
+
providers: Record<string, OmpProviderBlock>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* omp validates model entries strictly. These are its documented effort
|
|
36
|
+
* values; omit an unknown value rather than invalidating the whole provider.
|
|
37
|
+
*/
|
|
38
|
+
const OMP_EFFORT_VOCABULARY = new Set(["minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
39
|
+
|
|
40
|
+
function ompEfforts(model: ExportModel): string[] {
|
|
41
|
+
const efforts: string[] = [];
|
|
42
|
+
for (const effort of model.reasoningEfforts ?? []) {
|
|
43
|
+
const normalized = effort.trim().toLowerCase();
|
|
44
|
+
if (OMP_EFFORT_VOCABULARY.has(normalized) && !efforts.includes(normalized)) {
|
|
45
|
+
efforts.push(normalized);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return efforts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* omp's models.yml is Pi-like, but it supports effort metadata and a per-model
|
|
53
|
+
* API dialect. Native OpenAI models use Responses; all routed models inherit
|
|
54
|
+
* the provider's existing Chat Completions dialect.
|
|
55
|
+
*/
|
|
56
|
+
export function buildOmpClientConfig(ctx: ExportContext): OmpGeneratedConfig {
|
|
57
|
+
const models: OmpModelEntry[] = [];
|
|
58
|
+
for (const model of normalizeExportModels(ctx.models)) {
|
|
59
|
+
const input = inputModalitiesForClient("pi", model.inputModalities);
|
|
60
|
+
if (input === null) continue;
|
|
61
|
+
const entry: OmpModelEntry = {
|
|
62
|
+
id: model.namespaced,
|
|
63
|
+
name: exportModelLabel(model),
|
|
64
|
+
input,
|
|
65
|
+
...(model.native && model.provider === "openai" ? { api: "openai-responses" } : {}),
|
|
66
|
+
};
|
|
67
|
+
const context = authoritativeContextWindow(model.contextWindow);
|
|
68
|
+
if (context !== undefined) {
|
|
69
|
+
entry.contextWindow = context;
|
|
70
|
+
entry.maxTokens = outputBudgetFor(context);
|
|
71
|
+
}
|
|
72
|
+
const efforts = ompEfforts(model);
|
|
73
|
+
if (efforts.length > 0) {
|
|
74
|
+
const defaultLevel = model.defaultReasoningEffort?.trim().toLowerCase();
|
|
75
|
+
entry.reasoning = true;
|
|
76
|
+
entry.thinking = {
|
|
77
|
+
mode: "effort",
|
|
78
|
+
efforts,
|
|
79
|
+
...(defaultLevel && efforts.includes(defaultLevel) ? { defaultLevel } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
models.push(entry);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
providers: {
|
|
86
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
87
|
+
baseUrl: ctx.baseUrl,
|
|
88
|
+
api: PI_API_DIALECT,
|
|
89
|
+
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
90
|
+
models,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function summarizeOmp(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
97
|
+
const models = (document as OmpGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
|
|
98
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildOmpContribution(ctx: ExportContext): ManagedContribution {
|
|
102
|
+
const doc = buildOmpClientConfig(ctx);
|
|
103
|
+
return singleFragment("omp", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
104
|
+
}
|