@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906
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-VGeQEZ_v.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 +54 -24
- 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/identity.ts +8 -2
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +259 -29
- 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 +47 -13
- 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 +49 -9
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +15 -4
- 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/observe.ts +3 -1
- 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 +272 -32
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +44 -5
- package/src/codex/catalog/metadata.ts +149 -14
- package/src/codex/catalog/native-models.ts +116 -4
- package/src/codex/catalog/parsing.ts +122 -8
- package/src/codex/catalog/provider-fetch.ts +154 -23
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +99 -34
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- 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 +474 -178
- 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/account-quota-rank.ts +40 -1
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +140 -29
- package/src/oauth/log.ts +3 -0
- 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/muse-subscription-usage.ts +95 -0
- 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 +625 -98
- 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 +122 -28
- package/src/server/live.ts +18 -4
- 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 +45 -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 +510 -57
- package/src/server/responses/empty-completion-guard.ts +4 -0
- 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/request.ts +8 -0
- 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-B2YjLA-i.css +0 -1
- package/gui/dist/assets/index-aPup8CKb.js +0 -112
|
@@ -1,10 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mutatePersistedConfig } from "../config";
|
|
2
2
|
import { projectModelRenames } from "./model-rename-migration";
|
|
3
3
|
import type { OcxConfig } from "../types";
|
|
4
4
|
|
|
5
5
|
export interface ModelRenameStartupDeps {
|
|
6
6
|
project: typeof projectModelRenames;
|
|
7
|
-
|
|
7
|
+
/** Injected writer for tests and callers that own their own persistence. */
|
|
8
|
+
save?: (config: OcxConfig) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Recursive key-by-key adopt: descend into a changed container, replace only changed leaves. */
|
|
16
|
+
function adoptRecord(live: Record<string, unknown>, next: Record<string, unknown>): void {
|
|
17
|
+
for (const key of Object.keys(live)) {
|
|
18
|
+
if (!(key in next)) delete live[key];
|
|
19
|
+
}
|
|
20
|
+
for (const [key, value] of Object.entries(next)) {
|
|
21
|
+
const current = live[key];
|
|
22
|
+
if (JSON.stringify(current) === JSON.stringify(value)) continue;
|
|
23
|
+
if (isPlainObject(current) && isPlainObject(value)) {
|
|
24
|
+
adoptRecord(current, value);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
live[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Copy `source` onto `target` in place, touching only the keys that actually differ.
|
|
33
|
+
*
|
|
34
|
+
* A clear-and-reassign preserves the top-level object identity while silently detaching every
|
|
35
|
+
* nested sub-object a caller still holds a live reference to. The renames rewrite one provider
|
|
36
|
+
* row at a time, so every sibling row — and the `providers` container itself — must survive
|
|
37
|
+
* with its identity intact.
|
|
38
|
+
*/
|
|
39
|
+
function adoptConfig(target: OcxConfig, source: OcxConfig): void {
|
|
40
|
+
if (target === source) return;
|
|
41
|
+
adoptRecord(
|
|
42
|
+
target as unknown as Record<string, unknown>,
|
|
43
|
+
structuredClone(source) as unknown as Record<string, unknown>,
|
|
44
|
+
);
|
|
8
45
|
}
|
|
9
46
|
|
|
10
47
|
/**
|
|
@@ -15,14 +52,41 @@ export interface ModelRenameStartupDeps {
|
|
|
15
52
|
* recoverable from the config alone. This one only rewrites model ids that the
|
|
16
53
|
* registry itself no longer seeds, and the pre-migration value is a string this
|
|
17
54
|
* file still names, so the change is reversible by hand.
|
|
55
|
+
*
|
|
56
|
+
* Persistence failure is not fatal. This runs inside `startServer`, so a missing, malformed or
|
|
57
|
+
* contended config degrades to a warning plus the in-memory projection rather than taking the
|
|
58
|
+
* boot path down.
|
|
18
59
|
*/
|
|
19
60
|
export function runModelRenameStartupMigration(
|
|
20
61
|
config: OcxConfig,
|
|
21
|
-
deps: ModelRenameStartupDeps = { project: projectModelRenames
|
|
62
|
+
deps: ModelRenameStartupDeps = { project: projectModelRenames },
|
|
22
63
|
): OcxConfig {
|
|
23
|
-
const projection = deps.project(config);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
64
|
+
const projection = deps.project(structuredClone(config));
|
|
65
|
+
if (!projection.changed) {
|
|
66
|
+
for (const warning of projection.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
67
|
+
return config;
|
|
68
|
+
}
|
|
69
|
+
if (deps.save) {
|
|
70
|
+
deps.save(projection.config);
|
|
71
|
+
adoptConfig(config, projection.config);
|
|
72
|
+
for (const warning of projection.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
73
|
+
return config;
|
|
74
|
+
}
|
|
75
|
+
const outcome = mutatePersistedConfig(fresh => {
|
|
76
|
+
const next = deps.project(fresh);
|
|
77
|
+
if (next.changed) adoptConfig(fresh, next.config);
|
|
78
|
+
return { changed: next.changed, value: next };
|
|
79
|
+
});
|
|
80
|
+
if (outcome.status === "unavailable") {
|
|
81
|
+
console.warn(
|
|
82
|
+
`[model-rename-migration] persistence unavailable (${outcome.reason}); applying the renames `
|
|
83
|
+
+ "in memory for this run only.",
|
|
84
|
+
);
|
|
85
|
+
adoptConfig(config, projection.config);
|
|
86
|
+
for (const warning of projection.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
87
|
+
return config;
|
|
88
|
+
}
|
|
89
|
+
adoptConfig(config, outcome.value.config);
|
|
90
|
+
for (const warning of outcome.value.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
91
|
+
return config;
|
|
28
92
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta's subscription-usage SSE frame.
|
|
3
|
+
*
|
|
4
|
+
* Meta publishes no quota endpoint — 17 plausible REST paths were probed and every one
|
|
5
|
+
* 404s, and no `x-ratelimit-*` header appears on any of three measured request shapes
|
|
6
|
+
* (devlog/_plan/260903_muse_spark_plan_oauth/003 §E). The only machine-readable usage
|
|
7
|
+
* Meta emits arrives mid-stream, as one extra event alongside the ordinary
|
|
8
|
+
* `response.*` sequence on a streaming `POST /v1/responses`.
|
|
9
|
+
*
|
|
10
|
+
* That inverts the usual seam: this module is fed by the request path, not by a probe,
|
|
11
|
+
* and nothing can refresh its output on demand — obtaining a newer value would mean
|
|
12
|
+
* spending a real inference turn.
|
|
13
|
+
*
|
|
14
|
+
* Measured payload (2026-09-03):
|
|
15
|
+
*
|
|
16
|
+
* ```json
|
|
17
|
+
* { "type": "response.subscription_usage",
|
|
18
|
+
* "subscription": {
|
|
19
|
+
* "tier": "27681393394859588",
|
|
20
|
+
* "window": { "used_percent": 0, "resets_at": 1788431188, "window_duration_mins": 300 },
|
|
21
|
+
* "weekly": { "used_percent": 0, "resets_at": 1788739200 } } }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import { asRecord, normalizePercent, normalizeResetAt, toFiniteNumber } from "./quota-wire";
|
|
25
|
+
import type { ProviderQuota, ProviderQuotaWindow } from "./quota-types";
|
|
26
|
+
|
|
27
|
+
/** The SSE frame type Meta emits on streaming turns. */
|
|
28
|
+
export const MUSE_SUBSCRIPTION_USAGE_TYPE = "response.subscription_usage";
|
|
29
|
+
|
|
30
|
+
/** Meta's five-hour window, identified by its declared duration rather than assumed. */
|
|
31
|
+
const FIVE_HOUR_WINDOW_MINS = 300;
|
|
32
|
+
|
|
33
|
+
/** True when a parsed SSE payload is the subscription-usage frame. */
|
|
34
|
+
export function isMuseSubscriptionUsagePayload(payload: unknown): boolean {
|
|
35
|
+
return asRecord(payload)?.type === MUSE_SUBSCRIPTION_USAGE_TYPE;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Translate the frame into a `ProviderQuota`.
|
|
40
|
+
*
|
|
41
|
+
* Returns null — never throws — for anything unrecognizable. This runs inside SSE
|
|
42
|
+
* inspection on a live request, where the only acceptable failure is silence: a parse
|
|
43
|
+
* error must not cost the user their turn.
|
|
44
|
+
*
|
|
45
|
+
* `subscription.tier` is deliberately dropped. It is an opaque numeric id, not the plan
|
|
46
|
+
* label the Muse CLI prints, so surfacing it would show a meaningless number.
|
|
47
|
+
*/
|
|
48
|
+
export function parseMuseSubscriptionUsage(payload: unknown): ProviderQuota | null {
|
|
49
|
+
const subscription = asRecord(asRecord(payload)?.subscription);
|
|
50
|
+
if (!subscription) return null;
|
|
51
|
+
|
|
52
|
+
const quota: ProviderQuota = { updatedAt: Date.now() };
|
|
53
|
+
let sawWindow = false;
|
|
54
|
+
|
|
55
|
+
const window = asRecord(subscription.window);
|
|
56
|
+
if (window) {
|
|
57
|
+
const percent = normalizePercent(window.used_percent);
|
|
58
|
+
const resetAt = normalizeResetAt(window.resets_at);
|
|
59
|
+
const durationMins = toFiniteNumber(window.window_duration_mins);
|
|
60
|
+
if (percent !== undefined) {
|
|
61
|
+
if (durationMins === FIVE_HOUR_WINDOW_MINS) {
|
|
62
|
+
quota.fiveHourPercent = percent;
|
|
63
|
+
if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
|
|
64
|
+
sawWindow = true;
|
|
65
|
+
} else {
|
|
66
|
+
// A window of some other length is NOT forced into the five-hour slot: filing a
|
|
67
|
+
// ten-hour window there would understate usage by the ratio of the two windows,
|
|
68
|
+
// and would do so with full confidence. Carry it with its real duration instead.
|
|
69
|
+
const custom: ProviderQuotaWindow = {
|
|
70
|
+
label: durationMins === undefined ? "subscription" : `${durationMins}m`,
|
|
71
|
+
percent,
|
|
72
|
+
...(resetAt !== undefined ? { resetAt } : {}),
|
|
73
|
+
};
|
|
74
|
+
quota.customWindows = [...(quota.customWindows ?? []), custom];
|
|
75
|
+
sawWindow = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const weekly = asRecord(subscription.weekly);
|
|
81
|
+
if (weekly) {
|
|
82
|
+
const percent = normalizePercent(weekly.used_percent);
|
|
83
|
+
if (percent !== undefined) {
|
|
84
|
+
quota.weeklyPercent = percent;
|
|
85
|
+
const resetAt = normalizeResetAt(weekly.resets_at);
|
|
86
|
+
if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
|
|
87
|
+
sawWindow = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Either window may be absent independently, but a payload carrying neither says
|
|
92
|
+
// nothing — returning a bare `updatedAt` would publish an empty row that the GUI
|
|
93
|
+
// would render as a quota with no bars.
|
|
94
|
+
return sawWindow ? quota : null;
|
|
95
|
+
}
|
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
resolveCodexAuthContext,
|
|
8
8
|
type CodexAccountSelectionAdmission,
|
|
9
9
|
type CodexAuthContext,
|
|
10
|
+
type CodexAuthPolicyConfig,
|
|
10
11
|
} from "../codex/auth-context";
|
|
11
12
|
import { recordCodexUpstreamOutcome, type CodexUpstreamOutcome } from "../codex/routing";
|
|
12
13
|
import { extractAccountId } from "../oauth/chatgpt";
|
|
13
|
-
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../server/auth-cors";
|
|
14
|
+
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential, type DataPlaneAdmission } from "../server/auth-cors";
|
|
14
15
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
15
16
|
import {
|
|
16
17
|
CODEX_FORWARD_BASE_URL,
|
|
@@ -78,6 +79,8 @@ export function listOpenAiForwardSidecarCandidates(config: OcxConfig): OpenAiFor
|
|
|
78
79
|
|
|
79
80
|
function directSidecarHeaders(
|
|
80
81
|
incomingHeaders: Headers,
|
|
82
|
+
config: CodexAuthPolicyConfig,
|
|
83
|
+
admission?: Pick<DataPlaneAdmission, "source">,
|
|
81
84
|
): Headers | undefined {
|
|
82
85
|
const bearer = incomingHeaders.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
83
86
|
if (!bearer) return undefined;
|
|
@@ -89,7 +92,7 @@ function directSidecarHeaders(
|
|
|
89
92
|
// intentional ChatGPT-auth operation instead of silently reclassifying any JWT-shaped
|
|
90
93
|
// provider credential as a Codex bearer.
|
|
91
94
|
if (!requestedAccountId || requestedAccountId !== derivedAccountId) return undefined;
|
|
92
|
-
const selected = headersForCodexAuthContext(incomingHeaders, { kind: "main", accountId: null });
|
|
95
|
+
const selected = headersForCodexAuthContext(incomingHeaders, { kind: "main", accountId: null }, config, undefined, admission);
|
|
93
96
|
return selected;
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -99,10 +102,13 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
99
102
|
config: OcxConfig,
|
|
100
103
|
options: {
|
|
101
104
|
exactAccount?: ExactOpenAiSidecarAccount;
|
|
105
|
+
admission?: Pick<DataPlaneAdmission, "source">;
|
|
106
|
+
codexAuthPolicy?: CodexAuthPolicyConfig;
|
|
102
107
|
beginCodexAccountSelection?: () => CodexAccountSelectionAdmission | undefined;
|
|
103
108
|
} = {},
|
|
104
109
|
): Promise<ResolvedOpenAiForwardSidecar | undefined> {
|
|
105
110
|
const { exactAccount } = options;
|
|
111
|
+
const policy = options.codexAuthPolicy ?? config;
|
|
106
112
|
let callerBearerMayBeForwarded = true;
|
|
107
113
|
try {
|
|
108
114
|
validateForwardAdmissionCredential(incomingHeaders, config);
|
|
@@ -116,10 +122,13 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
116
122
|
// credential directly even when the provider is globally Direct, and never
|
|
117
123
|
// consult Pool active state, affinity, probes, or alternates.
|
|
118
124
|
const authContext = await resolveCodexAuthContext(incomingHeaders, config, "pool", {
|
|
125
|
+
codexAuthPolicy: policy,
|
|
119
126
|
accountId: exactAccount.accountId,
|
|
120
127
|
modelId: exactAccount.modelId,
|
|
128
|
+
admission: options.admission,
|
|
121
129
|
beginCodexAccountSelection: options.beginCodexAccountSelection,
|
|
122
130
|
});
|
|
131
|
+
const selectedHeaders = headersForCodexAuthContext(incomingHeaders, authContext, policy, exactAccount.modelId, options.admission);
|
|
123
132
|
if ((authContext.kind !== "pool" && authContext.kind !== "main-pool")
|
|
124
133
|
|| !isCodexAuthContextUsable(authContext, config)) {
|
|
125
134
|
// Exact selection is fail-closed. A generation/runtime-state race must not fall through
|
|
@@ -129,7 +138,7 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
129
138
|
return {
|
|
130
139
|
...candidate,
|
|
131
140
|
authContext,
|
|
132
|
-
headers:
|
|
141
|
+
headers: selectedHeaders,
|
|
133
142
|
recordOutcome: (outcome: CodexUpstreamOutcome) => recordCodexUpstreamOutcome(
|
|
134
143
|
config,
|
|
135
144
|
authContext.accountId,
|
|
@@ -149,7 +158,7 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
149
158
|
}
|
|
150
159
|
if (candidate.accountMode === "direct") {
|
|
151
160
|
if (!callerBearerMayBeForwarded || !hasCallerCodexBearer(incomingHeaders)) continue;
|
|
152
|
-
const headers = directSidecarHeaders(incomingHeaders);
|
|
161
|
+
const headers = directSidecarHeaders(incomingHeaders, policy, options.admission);
|
|
153
162
|
if (!headers) continue;
|
|
154
163
|
return {
|
|
155
164
|
...candidate,
|
|
@@ -158,13 +167,16 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
158
167
|
};
|
|
159
168
|
}
|
|
160
169
|
const authContext = await resolveCodexAuthContext(incomingHeaders, config, candidate.accountMode, {
|
|
170
|
+
codexAuthPolicy: policy,
|
|
171
|
+
admission: options.admission,
|
|
161
172
|
beginCodexAccountSelection: options.beginCodexAccountSelection,
|
|
162
173
|
});
|
|
174
|
+
const selectedHeaders = headersForCodexAuthContext(incomingHeaders, authContext, policy, undefined, options.admission);
|
|
163
175
|
if (!isCodexAuthContextUsable(authContext, config)) continue;
|
|
164
176
|
return {
|
|
165
177
|
...candidate,
|
|
166
178
|
authContext,
|
|
167
|
-
headers:
|
|
179
|
+
headers: selectedHeaders,
|
|
168
180
|
...(authContext.kind === "pool" || authContext.kind === "main-pool"
|
|
169
181
|
? {
|
|
170
182
|
recordOutcome: (outcome: CodexUpstreamOutcome) => recordCodexUpstreamOutcome(
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
import { openaiResponsesUrl } from "../adapters/openai-responses-url";
|
|
3
|
+
|
|
4
|
+
export const OPENAI_CODEX_PROVIDER_ID = "openai";
|
|
5
|
+
export const LEGACY_OPENAI_MULTI_PROVIDER_ID = "openai-multi";
|
|
6
|
+
export const OPENAI_API_PROVIDER_ID = "openai-apikey";
|
|
7
|
+
export const LEGACY_CHATGPT_PROVIDER_ID = "chatgpt";
|
|
8
|
+
|
|
9
|
+
export const CODEX_FORWARD_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
10
|
+
|
|
11
|
+
function normalizedBaseUrl(value: string): string | undefined {
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(value.trim());
|
|
14
|
+
if (url.username || url.password || url.search || url.hash) return undefined;
|
|
15
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
16
|
+
return `${url.origin}${path}`;
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isCanonicalOpenAiForwardProvider(provider: OcxProviderConfig): boolean {
|
|
23
|
+
return provider.adapter === "openai-responses"
|
|
24
|
+
&& provider.authMode === "forward"
|
|
25
|
+
&& normalizedBaseUrl(provider.baseUrl) === CODEX_FORWARD_BASE_URL;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const OPENAI_API_ORIGIN = "https://api.openai.com";
|
|
29
|
+
const OPENAI_API_BASE_URL = `${OPENAI_API_ORIGIN}/v1`;
|
|
30
|
+
const OPENAI_API_RESPONSES_URL = `${OPENAI_API_BASE_URL}/responses`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The Responses endpoint the adapter would actually POST key-auth traffic to, normalized.
|
|
34
|
+
*
|
|
35
|
+
* Mirrors the adapter's own construction (`src/adapters/openai-responses.ts`): a configured
|
|
36
|
+
* `responsesPath` is appended to the base verbatim, and only the default branch runs the
|
|
37
|
+
* `/v1/responses` suffix normalization. Classifying on the base URL alone would call
|
|
38
|
+
* `baseUrl: "https://api.openai.com"` with `responsesPath: "/other"` official even though that
|
|
39
|
+
* request never reaches the official Responses endpoint.
|
|
40
|
+
*/
|
|
41
|
+
function resolvedResponsesEndpoint(provider: OcxProviderConfig): string | undefined {
|
|
42
|
+
try {
|
|
43
|
+
const raw = provider.responsesPath === undefined
|
|
44
|
+
? openaiResponsesUrl(provider.baseUrl)
|
|
45
|
+
: `${provider.baseUrl.replace(/\/$/, "")}${provider.responsesPath}`;
|
|
46
|
+
return normalizedBaseUrl(raw);
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isOfficialOpenAiResponsesDestination(provider: OcxProviderConfig): boolean {
|
|
53
|
+
// Exact normalized URL keeps lookalike/suffix hosts out of this set: `api.openai.com.evil.test`
|
|
54
|
+
// resolves to its own origin, never to the official one.
|
|
55
|
+
return resolvedResponsesEndpoint(provider) === OPENAI_API_RESPONSES_URL;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether this provider can serve `POST /responses/compact`. The canonical ChatGPT
|
|
60
|
+
* backend can, and so can the official OpenAI API — but an arbitrary gateway that
|
|
61
|
+
* merely speaks the Responses wire cannot, and calling it there fails compaction
|
|
62
|
+
* with an unhelpful error instead of falling back to a routed summary (#422).
|
|
63
|
+
*/
|
|
64
|
+
export function supportsNativeResponsesCompactEndpoint(
|
|
65
|
+
providerName: string,
|
|
66
|
+
provider: OcxProviderConfig,
|
|
67
|
+
): boolean {
|
|
68
|
+
if (isCanonicalOpenAiForwardProvider(provider)) return true;
|
|
69
|
+
return providerName === OPENAI_API_PROVIDER_ID
|
|
70
|
+
&& provider.adapter === "openai-responses"
|
|
71
|
+
&& normalizedBaseUrl(provider.baseUrl) === OPENAI_API_BASE_URL;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether this destination is an OpenAI-operated Responses backend — the canonical ChatGPT Codex
|
|
76
|
+
* surface or the official OpenAI API.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately not keyed on `authMode === "forward"`: a noncanonical forward provider does not
|
|
79
|
+
* receive the caller's credentials (see the forward-header gate in the Responses adapter), so
|
|
80
|
+
* forward auth says nothing about which backend is on the other end.
|
|
81
|
+
*/
|
|
82
|
+
export function isOpenAiOperatedResponsesDestination(provider: OcxProviderConfig): boolean {
|
|
83
|
+
if (isCanonicalOpenAiForwardProvider(provider)) return true;
|
|
84
|
+
return provider.adapter === "openai-responses"
|
|
85
|
+
&& isOfficialOpenAiResponsesDestination(provider);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether this destination can decode a native (non-`ocx1:`) compaction blob.
|
|
90
|
+
*
|
|
91
|
+
* Only the backend that minted a blob can decode it. `authMode: "forward"` alone is not a signal:
|
|
92
|
+
* the adapter forwards caller credentials only to the canonical ChatGPT Codex surface, while a
|
|
93
|
+
* noncanonical forward provider receives no caller credentials and may point at any backend.
|
|
94
|
+
*
|
|
95
|
+
* Relay only to an OpenAI-operated destination or a destination whose operator explicitly opts in.
|
|
96
|
+
* Keyed by destination rather than provider id: a blob's issuer is the URL that produced it, not the
|
|
97
|
+
* local config key a replay travels under.
|
|
98
|
+
*/
|
|
99
|
+
export function destinationDecodesNativeCompactionBlob(provider: OcxProviderConfig): boolean {
|
|
100
|
+
return isOpenAiOperatedResponsesDestination(provider)
|
|
101
|
+
|| provider.decodesNativeCompactionBlobs === true;
|
|
102
|
+
}
|
|
@@ -2,13 +2,9 @@ import type { CodexAccountMode, OcxConfig, OcxProviderConfig, ProviderCostOverla
|
|
|
2
2
|
import { OPENAI_PROVIDER_TIER_VERSION } from "../types";
|
|
3
3
|
import { openaiResponsesUrl } from "../adapters/openai-responses-url";
|
|
4
4
|
import { MAX_COST4_RATE } from "../usage/expected-prices";
|
|
5
|
+
import { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "./openai-tiers-destination";
|
|
6
|
+
export { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, isOpenAiOperatedResponsesDestination, destinationDecodesNativeCompactionBlob } from "./openai-tiers-destination";
|
|
5
7
|
|
|
6
|
-
export const OPENAI_CODEX_PROVIDER_ID = "openai";
|
|
7
|
-
export const LEGACY_OPENAI_MULTI_PROVIDER_ID = "openai-multi";
|
|
8
|
-
export const OPENAI_API_PROVIDER_ID = "openai-apikey";
|
|
9
|
-
export const LEGACY_CHATGPT_PROVIDER_ID = "chatgpt";
|
|
10
|
-
|
|
11
|
-
export const CODEX_FORWARD_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
12
8
|
const LEGACY_OPENAI_MULTI_PREFIX = `${LEGACY_OPENAI_MULTI_PROVIDER_ID}/`;
|
|
13
9
|
|
|
14
10
|
function canonicalCodexForwardProvider(mode: CodexAccountMode): OcxProviderConfig {
|
|
@@ -20,99 +16,6 @@ function canonicalCodexForwardProvider(mode: CodexAccountMode): OcxProviderConfi
|
|
|
20
16
|
};
|
|
21
17
|
}
|
|
22
18
|
|
|
23
|
-
function normalizedBaseUrl(value: string): string | undefined {
|
|
24
|
-
try {
|
|
25
|
-
const url = new URL(value.trim());
|
|
26
|
-
if (url.username || url.password || url.search || url.hash) return undefined;
|
|
27
|
-
const path = url.pathname.replace(/\/+$/, "");
|
|
28
|
-
return `${url.origin}${path}`;
|
|
29
|
-
} catch {
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function isCanonicalOpenAiForwardProvider(provider: OcxProviderConfig): boolean {
|
|
35
|
-
return provider.adapter === "openai-responses"
|
|
36
|
-
&& provider.authMode === "forward"
|
|
37
|
-
&& normalizedBaseUrl(provider.baseUrl) === CODEX_FORWARD_BASE_URL;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const OPENAI_API_ORIGIN = "https://api.openai.com";
|
|
41
|
-
const OPENAI_API_BASE_URL = `${OPENAI_API_ORIGIN}/v1`;
|
|
42
|
-
const OPENAI_API_RESPONSES_URL = `${OPENAI_API_BASE_URL}/responses`;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* The Responses endpoint the adapter would actually POST key-auth traffic to, normalized.
|
|
46
|
-
*
|
|
47
|
-
* Mirrors the adapter's own construction (`src/adapters/openai-responses.ts`): a configured
|
|
48
|
-
* `responsesPath` is appended to the base verbatim, and only the default branch runs the
|
|
49
|
-
* `/v1/responses` suffix normalization. Classifying on the base URL alone would call
|
|
50
|
-
* `baseUrl: "https://api.openai.com"` with `responsesPath: "/other"` official even though that
|
|
51
|
-
* request never reaches the official Responses endpoint.
|
|
52
|
-
*/
|
|
53
|
-
function resolvedResponsesEndpoint(provider: OcxProviderConfig): string | undefined {
|
|
54
|
-
try {
|
|
55
|
-
const raw = provider.responsesPath === undefined
|
|
56
|
-
? openaiResponsesUrl(provider.baseUrl)
|
|
57
|
-
: `${provider.baseUrl.replace(/\/$/, "")}${provider.responsesPath}`;
|
|
58
|
-
return normalizedBaseUrl(raw);
|
|
59
|
-
} catch {
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function isOfficialOpenAiResponsesDestination(provider: OcxProviderConfig): boolean {
|
|
65
|
-
// Exact normalized URL keeps lookalike/suffix hosts out of this set: `api.openai.com.evil.test`
|
|
66
|
-
// resolves to its own origin, never to the official one.
|
|
67
|
-
return resolvedResponsesEndpoint(provider) === OPENAI_API_RESPONSES_URL;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Whether this provider can serve `POST /responses/compact`. The canonical ChatGPT
|
|
72
|
-
* backend can, and so can the official OpenAI API — but an arbitrary gateway that
|
|
73
|
-
* merely speaks the Responses wire cannot, and calling it there fails compaction
|
|
74
|
-
* with an unhelpful error instead of falling back to a routed summary (#422).
|
|
75
|
-
*/
|
|
76
|
-
export function supportsNativeResponsesCompactEndpoint(
|
|
77
|
-
providerName: string,
|
|
78
|
-
provider: OcxProviderConfig,
|
|
79
|
-
): boolean {
|
|
80
|
-
if (isCanonicalOpenAiForwardProvider(provider)) return true;
|
|
81
|
-
return providerName === OPENAI_API_PROVIDER_ID
|
|
82
|
-
&& provider.adapter === "openai-responses"
|
|
83
|
-
&& normalizedBaseUrl(provider.baseUrl) === OPENAI_API_BASE_URL;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Whether this destination is an OpenAI-operated Responses backend — the canonical ChatGPT Codex
|
|
88
|
-
* surface or the official OpenAI API.
|
|
89
|
-
*
|
|
90
|
-
* Deliberately not keyed on `authMode === "forward"`: a noncanonical forward provider does not
|
|
91
|
-
* receive the caller's credentials (see the forward-header gate in the Responses adapter), so
|
|
92
|
-
* forward auth says nothing about which backend is on the other end.
|
|
93
|
-
*/
|
|
94
|
-
export function isOpenAiOperatedResponsesDestination(provider: OcxProviderConfig): boolean {
|
|
95
|
-
if (isCanonicalOpenAiForwardProvider(provider)) return true;
|
|
96
|
-
return provider.adapter === "openai-responses"
|
|
97
|
-
&& isOfficialOpenAiResponsesDestination(provider);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Whether this destination can decode a native (non-`ocx1:`) compaction blob.
|
|
102
|
-
*
|
|
103
|
-
* Only the backend that minted a blob can decode it. `authMode: "forward"` alone is not a signal:
|
|
104
|
-
* the adapter forwards caller credentials only to the canonical ChatGPT Codex surface, while a
|
|
105
|
-
* noncanonical forward provider receives no caller credentials and may point at any backend.
|
|
106
|
-
*
|
|
107
|
-
* Relay only to an OpenAI-operated destination or a destination whose operator explicitly opts in.
|
|
108
|
-
* Keyed by destination rather than provider id: a blob's issuer is the URL that produced it, not the
|
|
109
|
-
* local config key a replay travels under.
|
|
110
|
-
*/
|
|
111
|
-
export function destinationDecodesNativeCompactionBlob(provider: OcxProviderConfig): boolean {
|
|
112
|
-
return isOpenAiOperatedResponsesDestination(provider)
|
|
113
|
-
|| provider.decodesNativeCompactionBlobs === true;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
19
|
export interface OpenAiTierMigrationProjection {
|
|
117
20
|
config: OcxConfig;
|
|
118
21
|
changed: boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { OcxProviderConfig } from "../types";
|
|
3
|
+
import { registryEntryForProviderDestination } from "./registry";
|
|
4
|
+
|
|
5
|
+
export const OPENCODE_GO_SESSION_HEADER = "x-opencode-session";
|
|
6
|
+
|
|
7
|
+
function hasHeaderCaseInsensitive(
|
|
8
|
+
headers: Record<string, string> | undefined,
|
|
9
|
+
name: string,
|
|
10
|
+
): boolean {
|
|
11
|
+
const target = name.toLowerCase();
|
|
12
|
+
return Object.keys(headers ?? {}).some(key => key.toLowerCase() === target);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Derive a provider-scoped opaque value without exposing Codex task or subagent ids. */
|
|
16
|
+
export function deriveOpenCodeGoSessionId(sessionLane: string): string {
|
|
17
|
+
const digest = createHash("sha256")
|
|
18
|
+
.update("opencodex/opencode-go/session/v1\0")
|
|
19
|
+
.update(sessionLane)
|
|
20
|
+
.digest("hex")
|
|
21
|
+
.slice(0, 32);
|
|
22
|
+
return `ocx_${digest}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Add per-conversation Go affinity only to the canonical fixed-key destination. */
|
|
26
|
+
export function resolveOpenCodeGoTransport<T extends OcxProviderConfig>(
|
|
27
|
+
provider: T,
|
|
28
|
+
sessionLane: string | undefined,
|
|
29
|
+
): T {
|
|
30
|
+
if (registryEntryForProviderDestination(provider)?.id !== "opencode-go") return provider;
|
|
31
|
+
if (!sessionLane) return provider;
|
|
32
|
+
if (hasHeaderCaseInsensitive(provider.headers, OPENCODE_GO_SESSION_HEADER)) return provider;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...provider,
|
|
36
|
+
headers: {
|
|
37
|
+
...(provider.headers ?? {}),
|
|
38
|
+
[OPENCODE_GO_SESSION_HEADER]: deriveOpenCodeGoSessionId(sessionLane),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|