@bitkyc08/opencodex 2.11.1 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/ocx.mjs +4 -0
- package/gui/dist/assets/index-BNVYzdn0.css +1 -0
- package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +94 -5
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google.ts +24 -7
- package/src/adapters/openai-chat.ts +233 -305
- package/src/adapters/openai-responses.ts +62 -0
- package/src/adapters/tool-catalog-nudge.ts +2 -11
- package/src/claude/outbound.ts +42 -14
- package/src/cli/claude.ts +61 -9
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +8 -1
- package/src/cli/config-command.ts +4 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +15 -0
- package/src/cli/index.ts +27 -11
- package/src/cli/lab.ts +367 -0
- package/src/cli/provider.ts +9 -1
- package/src/clients/config-export.ts +100 -3
- package/src/codex/app-server-processes.ts +11 -8
- package/src/codex/auth-api.ts +92 -28
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/catalog/metadata.ts +7 -3
- package/src/codex/catalog/provider-fetch.ts +191 -47
- package/src/codex/catalog.ts +2 -2
- package/src/codex/history-job.ts +83 -18
- package/src/codex/history-migration-guardian.ts +9 -16
- package/src/codex/history-provider.ts +178 -1
- package/src/codex/history-transition.ts +5 -5
- package/src/codex/history-worker.ts +22 -2
- package/src/codex/native-profile-store.ts +1 -1
- package/src/codex/plan.ts +15 -0
- package/src/codex/quota.ts +6 -6
- package/src/codex/routing.ts +3 -3
- package/src/codex/subagent-model-fallback.ts +246 -22
- package/src/codex/user-identity.ts +63 -12
- package/src/config.ts +265 -32
- package/src/images/loop.ts +3 -1
- package/src/images/plan.ts +23 -13
- package/src/integrations/omp-yaml-source.ts +225 -0
- package/src/integrations/ownership.ts +5 -5
- package/src/integrations/state.ts +45 -6
- package/src/integrations/writer.ts +38 -3
- 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/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 +24 -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 +160 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +57 -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 +191 -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 +228 -0
- package/src/lab/events/validate.ts +780 -0
- package/src/lab/index.ts +36 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +241 -0
- package/src/lab/ledger/store.ts +263 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +311 -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 +116 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +344 -0
- package/src/lab/observe/from-live.ts +103 -0
- package/src/lab/paths.ts +65 -0
- package/src/lab/projection/rebuild.ts +477 -0
- package/src/lab/projection/schema.ts +122 -0
- package/src/lab/projection/verdicts.ts +438 -0
- package/src/lab/projection/verification.ts +332 -0
- package/src/lab/query/catalog.ts +72 -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/index.ts +34 -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 +81 -0
- package/src/lab/subject/route-subject.ts +73 -0
- package/src/lib/bounded-body.ts +104 -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 +45 -0
- package/src/lib/pinned-http.ts +36 -7
- package/src/oauth/index.ts +20 -1
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/login-cli.ts +38 -4
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/codex-capacity.ts +7 -4
- package/src/providers/command-code-efforts.ts +5 -2
- package/src/providers/derive.ts +105 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/openai-tiers.ts +59 -3
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/quota.ts +67 -21
- package/src/providers/registry.ts +118 -5
- package/src/router.ts +15 -1
- package/src/routing/analytics.ts +8 -7
- package/src/routing/evaluator.ts +51 -5
- package/src/routing/profile.ts +10 -1
- package/src/routing/trace.ts +42 -6
- package/src/server/auth-cors.ts +15 -0
- package/src/server/background-lifecycle.ts +169 -0
- package/src/server/chat-completions.ts +18 -3
- package/src/server/claude-messages.ts +1 -0
- package/src/server/images.ts +78 -11
- package/src/server/index.ts +72 -18
- package/src/server/lifecycle.ts +6 -0
- package/src/server/live.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +41 -24
- package/src/server/management/body.ts +6 -0
- package/src/server/management/config-routes.ts +51 -8
- package/src/server/management/lab-routes.ts +388 -0
- package/src/server/management/logs-usage-routes.ts +19 -1
- package/src/server/management/model-rows.ts +25 -11
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +5 -0
- package/src/server/management/shared.ts +8 -1
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management/vision-sidecar-options.ts +125 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/request-decompress.ts +106 -5
- package/src/server/responses/collaboration.ts +4 -1
- package/src/server/responses/core.ts +117 -31
- package/src/server/responses-json-events.ts +50 -12
- package/src/server/search.ts +23 -7
- package/src/server/ws-bridge.ts +3 -1
- package/src/service.ts +40 -20
- package/src/types.ts +74 -0
- package/src/update/job.ts +10 -2
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +9 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/describe.ts +2 -2
- package/src/vision/eligibility.ts +229 -0
- package/src/vision/index.ts +20 -2
- package/src/web-search/executor.ts +2 -2
- package/src/web-search/index.ts +4 -2
- package/src/web-search/loop.ts +3 -1
- package/src/web-search/parse.ts +72 -3
- package/gui/dist/assets/index-BA1dgo4q.css +0 -1
- package/gui/dist/assets/index-DDZpgzKk.js +0 -70
package/src/oauth/login-cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { loadConfig, saveConfig } from "../config";
|
|
|
4
4
|
import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
|
|
5
5
|
import { isPublicOAuthProvider, listOAuthProviders, runLogin } from "./index";
|
|
6
6
|
import { KEY_LOGIN_PROVIDERS, isKeyLoginProvider, validateApiKey, type KeyLoginProvider } from "./key-providers";
|
|
7
|
-
import type { OcxProviderConfig } from "../types";
|
|
7
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
8
8
|
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
9
9
|
import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
|
|
10
10
|
|
|
@@ -102,10 +102,46 @@ export function providerConfigFromKeyLoginProvider(def: KeyLoginProvider, key: s
|
|
|
102
102
|
...(def.noPenaltyModels ? { noPenaltyModels: [...def.noPenaltyModels] } : {}),
|
|
103
103
|
...(def.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...def.autoToolChoiceOnlyModels] } : {}),
|
|
104
104
|
...(def.preserveReasoningContentModels ? { preserveReasoningContentModels: [...def.preserveReasoningContentModels] } : {}),
|
|
105
|
+
...(def.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...def.requiresReasoningPlaceholderModels] } : {}),
|
|
105
106
|
...(def.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: def.escapeBuiltinToolNames } : {}),
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Merge a freshly built key-login provider row with a previously saved row,
|
|
112
|
+
* carrying operator-owned fields the key-provider preset cannot know about.
|
|
113
|
+
* Currently that is the user-configured modelCosts overlay: rotating the API
|
|
114
|
+
* key must not silently revert Logs/Usage estimates to catalog prices.
|
|
115
|
+
*/
|
|
116
|
+
export function mergeKeyLoginProviderRow(
|
|
117
|
+
provider: OcxProviderConfig,
|
|
118
|
+
existing: OcxProviderConfig | undefined,
|
|
119
|
+
): OcxProviderConfig {
|
|
120
|
+
return {
|
|
121
|
+
...provider,
|
|
122
|
+
...(existing?.modelCosts !== undefined ? { modelCosts: existing.modelCosts } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Commit a fresh key-login provider row: merge operator-owned fields from the
|
|
128
|
+
* existing row (currently the modelCosts overlay), persist the merged row, and
|
|
129
|
+
* push the SAME merged row to a running proxy so its live config cannot diverge
|
|
130
|
+
* from disk (e.g. by replacing the overlay with catalog prices until reload).
|
|
131
|
+
* Returns the merged row that was persisted and notified.
|
|
132
|
+
*/
|
|
133
|
+
export async function commitKeyLoginProvider(
|
|
134
|
+
config: OcxConfig,
|
|
135
|
+
name: string,
|
|
136
|
+
provider: OcxProviderConfig,
|
|
137
|
+
): Promise<OcxProviderConfig> {
|
|
138
|
+
const mergedProvider = mergeKeyLoginProviderRow(provider, config.providers[name]);
|
|
139
|
+
config.providers[name] = mergedProvider;
|
|
140
|
+
saveConfig(config);
|
|
141
|
+
await notifyRunningProxy(name, mergedProvider);
|
|
142
|
+
return mergedProvider;
|
|
143
|
+
}
|
|
144
|
+
|
|
109
145
|
async function handleKeyLogin(name: string): Promise<void> {
|
|
110
146
|
const def = KEY_LOGIN_PROVIDERS[name];
|
|
111
147
|
const preflightConfig = loadConfig();
|
|
@@ -148,9 +184,7 @@ async function handleKeyLogin(name: string): Promise<void> {
|
|
|
148
184
|
console.error(`Error: ${commitCollision}.`);
|
|
149
185
|
process.exit(1);
|
|
150
186
|
}
|
|
151
|
-
config
|
|
152
|
-
saveConfig(config);
|
|
153
|
-
await notifyRunningProxy(name, provider);
|
|
187
|
+
await commitKeyLoginProvider(config, name, provider);
|
|
154
188
|
console.log(`✅ ${def.label} added. Try: ocx sync`);
|
|
155
189
|
}
|
|
156
190
|
|
|
@@ -24,7 +24,7 @@ const INTL_ID = "alibaba-token-plan-intl";
|
|
|
24
24
|
* the one matching the destination's registry contract.
|
|
25
25
|
* `defaultModel` and `note` are user-editable too and are handled below.
|
|
26
26
|
*/
|
|
27
|
-
const USER_OWNED_FIELDS = ["apiKey", "apiKeyPool", "disabled", "baseUrl", "allowPrivateNetwork", "liveModels"] as const;
|
|
27
|
+
const USER_OWNED_FIELDS = ["apiKey", "apiKeyPool", "disabled", "baseUrl", "allowPrivateNetwork", "liveModels", "modelCosts"] as const;
|
|
28
28
|
|
|
29
29
|
export interface AlibabaRegionMigrationProjection {
|
|
30
30
|
config: OcxConfig;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { codexPlanKey, codexPlanValue } from "../codex/plan";
|
|
2
|
+
|
|
1
3
|
export const CODEX_CONFIGURED_CAPACITY_WEIGHTS = {
|
|
2
4
|
plus: 1,
|
|
3
5
|
business: 1,
|
|
@@ -22,7 +24,7 @@ export type CodexCapacityQuota = {
|
|
|
22
24
|
export interface CodexCapacityAccount {
|
|
23
25
|
isMain: boolean;
|
|
24
26
|
active?: boolean;
|
|
25
|
-
plan?:
|
|
27
|
+
plan?: unknown;
|
|
26
28
|
paused: boolean;
|
|
27
29
|
needsReauth?: boolean;
|
|
28
30
|
quota: CodexCapacityQuota | null;
|
|
@@ -82,8 +84,8 @@ type MutableWindow = {
|
|
|
82
84
|
oldestUpdatedAt: number;
|
|
83
85
|
};
|
|
84
86
|
|
|
85
|
-
function configuredWeight(plan:
|
|
86
|
-
const normalized = plan
|
|
87
|
+
function configuredWeight(plan: unknown): number | undefined {
|
|
88
|
+
const normalized = codexPlanKey(plan);
|
|
87
89
|
return normalized && Object.hasOwn(CODEX_CONFIGURED_CAPACITY_WEIGHTS, normalized)
|
|
88
90
|
? CODEX_CONFIGURED_CAPACITY_WEIGHTS[normalized as keyof typeof CODEX_CONFIGURED_CAPACITY_WEIGHTS]
|
|
89
91
|
: undefined;
|
|
@@ -168,9 +170,10 @@ export function aggregateCodexPoolCapacity(
|
|
|
168
170
|
const current = accounts.find(account => account.active)
|
|
169
171
|
?? accounts.find(account => account.isMain)
|
|
170
172
|
?? accounts[0];
|
|
173
|
+
const currentPlan = codexPlanValue(current?.plan);
|
|
171
174
|
const currentAccount = current ? {
|
|
172
175
|
isMain: current.isMain,
|
|
173
|
-
...(
|
|
176
|
+
...(currentPlan !== undefined ? { plan: currentPlan } : {}),
|
|
174
177
|
quota: currentQuotaForDisplay(current, now),
|
|
175
178
|
} : undefined;
|
|
176
179
|
const windows = new Map<string, MutableWindow>();
|
|
@@ -31,7 +31,8 @@ function keyFor(modelId: string): string {
|
|
|
31
31
|
|
|
32
32
|
export function commandCodeReasoningEfforts(modelId: string): readonly string[] | undefined {
|
|
33
33
|
const key = keyFor(modelId);
|
|
34
|
-
return refreshedEfforts.get(key)
|
|
34
|
+
return refreshedEfforts.get(key)
|
|
35
|
+
?? (Object.hasOwn(COMMAND_CODE_MODEL_REASONING_EFFORTS, key) ? COMMAND_CODE_MODEL_REASONING_EFFORTS[key] : undefined);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function parsedProfileEfforts(page: string): string[] | undefined {
|
|
@@ -59,7 +60,9 @@ export async function refreshCommandCodeReasoningEfforts(
|
|
|
59
60
|
fetchFn: typeof globalThis.fetch = globalThis.fetch,
|
|
60
61
|
): Promise<readonly string[] | undefined> {
|
|
61
62
|
const key = keyFor(modelId);
|
|
62
|
-
const profile = COMMAND_CODE_MODEL_EFFORTS
|
|
63
|
+
const profile = Object.hasOwn(COMMAND_CODE_MODEL_EFFORTS, key)
|
|
64
|
+
? COMMAND_CODE_MODEL_EFFORTS[key as keyof typeof COMMAND_CODE_MODEL_EFFORTS]
|
|
65
|
+
: undefined;
|
|
63
66
|
if (!profile) return undefined;
|
|
64
67
|
try {
|
|
65
68
|
const response = await fetchFn(profile.profileUrl, {
|
package/src/providers/derive.ts
CHANGED
|
@@ -36,6 +36,7 @@ export interface DerivedKeyLoginProvider {
|
|
|
36
36
|
noPenaltyModels?: string[];
|
|
37
37
|
autoToolChoiceOnlyModels?: string[];
|
|
38
38
|
preserveReasoningContentModels?: string[];
|
|
39
|
+
requiresReasoningPlaceholderModels?: string[];
|
|
39
40
|
reasoningSplitModels?: string[];
|
|
40
41
|
thinkingToggleModels?: string[];
|
|
41
42
|
thinkingBudgetModels?: string[];
|
|
@@ -109,6 +110,94 @@ function cloneNestedRecord(input: Record<string, Record<string, string>>): Recor
|
|
|
109
110
|
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, { ...value }]));
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function sameStringArray(left: readonly string[] | undefined, right: readonly string[]): boolean {
|
|
114
|
+
return left?.length === right.length && left.every((value, index) => value === right[index]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type DirectReasoningEffortOverrides = Pick<
|
|
118
|
+
OcxProviderConfig,
|
|
119
|
+
"thinkingBudgetModels" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "modelReasoningEffortMap"
|
|
120
|
+
>;
|
|
121
|
+
|
|
122
|
+
function fillFoldedModelDefault<T>(
|
|
123
|
+
merged: Record<string, T> | undefined,
|
|
124
|
+
explicit: Record<string, T> | undefined,
|
|
125
|
+
model: string,
|
|
126
|
+
registryDefault: T | undefined,
|
|
127
|
+
clone: (value: T) => T,
|
|
128
|
+
): Record<string, T> | undefined {
|
|
129
|
+
const folded = model.toLowerCase();
|
|
130
|
+
const explicitEntries = Object.entries(explicit ?? {}).filter(([key]) => key.toLowerCase() === folded);
|
|
131
|
+
if (explicitEntries.length === 0) {
|
|
132
|
+
return registryDefault === undefined ? merged : { ...(merged ?? {}), [model]: clone(registryDefault) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const next = Object.fromEntries(
|
|
136
|
+
Object.entries(merged ?? {}).filter(([key]) => key.toLowerCase() !== folded),
|
|
137
|
+
) as Record<string, T>;
|
|
138
|
+
for (const [key, value] of explicitEntries) next[key] = clone(value);
|
|
139
|
+
return next;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Apply a registry-owned direct `reasoning_effort` contract after user/seed metadata is merged.
|
|
144
|
+
*
|
|
145
|
+
* Provider presets are persisted with capability metadata, so an existing config can retain an
|
|
146
|
+
* obsolete thinking-budget classification after the registry learns that a model has a native
|
|
147
|
+
* effort field. The registry opts only verified model contracts into this repair; providers that
|
|
148
|
+
* do not resolve through the matching registry entry keep their user-supplied classification.
|
|
149
|
+
*/
|
|
150
|
+
export function applyDirectReasoningEffortContracts(
|
|
151
|
+
entry: ProviderRegistryEntry,
|
|
152
|
+
prov: OcxProviderConfig,
|
|
153
|
+
explicit: DirectReasoningEffortOverrides = prov,
|
|
154
|
+
): void {
|
|
155
|
+
const models = entry.directReasoningEffortModels;
|
|
156
|
+
if (!models || models.length === 0) return;
|
|
157
|
+
|
|
158
|
+
for (const model of models) {
|
|
159
|
+
// Old generated presets persisted the direct model at the front of the registry's budget
|
|
160
|
+
// list. Routing merges registry-first, which moves that one stale entry to the end. Repair
|
|
161
|
+
// only those two exact generated shapes; any partial, reordered, or case-varied list is a
|
|
162
|
+
// deliberate user value and remains untouched.
|
|
163
|
+
const currentBudgetModels = entry.thinkingBudgetModels ?? [];
|
|
164
|
+
const staleSeedShape = [model, ...currentBudgetModels];
|
|
165
|
+
const staleRoutedShape = [...currentBudgetModels, model];
|
|
166
|
+
if (sameStringArray(explicit.thinkingBudgetModels, staleSeedShape)
|
|
167
|
+
|| sameStringArray(explicit.thinkingBudgetModels, staleRoutedShape)) {
|
|
168
|
+
prov.thinkingBudgetModels = [...currentBudgetModels];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const efforts = entry.modelReasoningEfforts?.[model];
|
|
172
|
+
prov.modelReasoningEfforts = fillFoldedModelDefault(
|
|
173
|
+
prov.modelReasoningEfforts,
|
|
174
|
+
explicit.modelReasoningEfforts,
|
|
175
|
+
model,
|
|
176
|
+
efforts,
|
|
177
|
+
value => [...value],
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const defaultEffort = entry.modelDefaultReasoningEfforts?.[model];
|
|
181
|
+
prov.modelDefaultReasoningEfforts = fillFoldedModelDefault(
|
|
182
|
+
prov.modelDefaultReasoningEfforts,
|
|
183
|
+
explicit.modelDefaultReasoningEfforts,
|
|
184
|
+
model,
|
|
185
|
+
defaultEffort,
|
|
186
|
+
value => value,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// An explicit empty model map masks any provider-wide aliases. Without it, a stale global
|
|
190
|
+
// mapping such as xhigh -> max would win before the verified direct ladder can clamp it.
|
|
191
|
+
prov.modelReasoningEffortMap = fillFoldedModelDefault(
|
|
192
|
+
prov.modelReasoningEffortMap,
|
|
193
|
+
explicit.modelReasoningEffortMap,
|
|
194
|
+
model,
|
|
195
|
+
{},
|
|
196
|
+
value => ({ ...value }),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
112
201
|
/**
|
|
113
202
|
* Build the provider config a registry entry contributes when a preset is materialized.
|
|
114
203
|
* The registry auth kind is preserved verbatim (including `"local"`) so fail-closed gates
|
|
@@ -152,8 +241,12 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
152
241
|
...(entry.promptCacheKey !== undefined ? { promptCacheKey: entry.promptCacheKey } : {}),
|
|
153
242
|
...(entry.responsesPath !== undefined ? { responsesPath: entry.responsesPath } : {}),
|
|
154
243
|
...(entry.statelessResponses !== undefined ? { statelessResponses: entry.statelessResponses } : {}),
|
|
244
|
+
...(entry.requiresAdjacentResponsesToolResults !== undefined
|
|
245
|
+
? { requiresAdjacentResponsesToolResults: entry.requiresAdjacentResponsesToolResults }
|
|
246
|
+
: {}),
|
|
155
247
|
...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
|
|
156
248
|
...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
|
|
249
|
+
...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}),
|
|
157
250
|
...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}),
|
|
158
251
|
...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
|
|
159
252
|
...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}),
|
|
@@ -199,6 +292,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
|
|
|
199
292
|
...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
|
|
200
293
|
...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
|
|
201
294
|
...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
|
|
295
|
+
...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}),
|
|
202
296
|
...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}),
|
|
203
297
|
...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
|
|
204
298
|
...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}),
|
|
@@ -293,6 +387,12 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
293
387
|
enrichReasoningSummariesByDestination(prov);
|
|
294
388
|
return;
|
|
295
389
|
}
|
|
390
|
+
const explicitDirectReasoning: DirectReasoningEffortOverrides = {
|
|
391
|
+
thinkingBudgetModels: prov.thinkingBudgetModels,
|
|
392
|
+
modelReasoningEfforts: prov.modelReasoningEfforts,
|
|
393
|
+
modelDefaultReasoningEfforts: prov.modelDefaultReasoningEfforts,
|
|
394
|
+
modelReasoningEffortMap: prov.modelReasoningEffortMap,
|
|
395
|
+
};
|
|
296
396
|
const seed = providerConfigSeed(entry);
|
|
297
397
|
if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
|
|
298
398
|
if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
|
|
@@ -323,6 +423,9 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
323
423
|
// learned this route still gets backfilled.
|
|
324
424
|
if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
|
|
325
425
|
if (prov.statelessResponses === undefined && seed.statelessResponses !== undefined) prov.statelessResponses = seed.statelessResponses;
|
|
426
|
+
if (prov.requiresAdjacentResponsesToolResults === undefined && seed.requiresAdjacentResponsesToolResults !== undefined) {
|
|
427
|
+
prov.requiresAdjacentResponsesToolResults = seed.requiresAdjacentResponsesToolResults;
|
|
428
|
+
}
|
|
326
429
|
// Registry-only metadata (never seeded into saved config): backfill straight from
|
|
327
430
|
// the entry so an explicit user value stays distinguishable from the default.
|
|
328
431
|
if (prov.supportsServiceTier === undefined && entry.supportsServiceTier !== undefined) prov.supportsServiceTier = entry.supportsServiceTier;
|
|
@@ -345,6 +448,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
345
448
|
}
|
|
346
449
|
if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels];
|
|
347
450
|
if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels];
|
|
451
|
+
if (!prov.requiresReasoningPlaceholderModels && seed.requiresReasoningPlaceholderModels) prov.requiresReasoningPlaceholderModels = [...seed.requiresReasoningPlaceholderModels];
|
|
348
452
|
if (!prov.reasoningSplitModels && seed.reasoningSplitModels) prov.reasoningSplitModels = [...seed.reasoningSplitModels];
|
|
349
453
|
if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels];
|
|
350
454
|
if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels];
|
|
@@ -353,6 +457,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
353
457
|
if (prov.freeTier === undefined && seed.freeTier !== undefined) prov.freeTier = seed.freeTier;
|
|
354
458
|
if (prov.modelSuffixBracketStrip === undefined && seed.modelSuffixBracketStrip !== undefined) prov.modelSuffixBracketStrip = seed.modelSuffixBracketStrip;
|
|
355
459
|
if (!prov.headers && seed.headers) prov.headers = { ...seed.headers };
|
|
460
|
+
applyDirectReasoningEffortContracts(entry, prov, explicitDirectReasoning);
|
|
356
461
|
}
|
|
357
462
|
|
|
358
463
|
export function deriveFeaturedProviderIds(): string[] {
|
package/src/providers/label.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
|
|
2
2
|
|
|
3
|
-
function canonicalUsageProviderLabel(provider: string): string {
|
|
3
|
+
export function canonicalUsageProviderLabel(provider: string): string {
|
|
4
4
|
return provider === "chatgpt" || provider === "openai-multi" ? "openai" : provider;
|
|
5
5
|
}
|
|
6
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
1
|
+
import type { CodexAccountMode, OcxConfig, OcxProviderConfig, ProviderCostOverlay } from "../types";
|
|
2
2
|
import { OPENAI_PROVIDER_TIER_VERSION } from "../types";
|
|
3
|
+
import { MAX_COST4_RATE } from "../usage/expected-prices";
|
|
3
4
|
|
|
4
5
|
export const OPENAI_CODEX_PROVIDER_ID = "openai";
|
|
5
6
|
export const LEGACY_OPENAI_MULTI_PROVIDER_ID = "openai-multi";
|
|
@@ -69,11 +70,17 @@ export class OpenAiTierMigrationCollisionError extends Error {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
function managedLegacyMultiOverlay(
|
|
73
|
-
|
|
73
|
+
function managedLegacyMultiOverlay(
|
|
74
|
+
provider: OcxProviderConfig,
|
|
75
|
+
): Pick<OcxProviderConfig, "disabled" | "selectedModels" | "modelCosts"> | null {
|
|
76
|
+
const allowed = new Set(["adapter", "authMode", "baseUrl", "disabled", "selectedModels", "modelCosts"]);
|
|
74
77
|
if (!Object.keys(provider).every(key => allowed.has(key))) return null;
|
|
75
78
|
if (!isCanonicalOpenAiForwardProvider(provider)) return null;
|
|
76
79
|
if (provider.disabled !== undefined && typeof provider.disabled !== "boolean") return null;
|
|
80
|
+
// Keep the migration self-contained: a malformed overlay is a collision, not
|
|
81
|
+
// something to carry into the canonical row (importing config.ts here would
|
|
82
|
+
// create an import cycle — config.ts already imports this module).
|
|
83
|
+
if (provider.modelCosts !== undefined && !validLegacyOverlayCosts(provider.modelCosts)) return null;
|
|
77
84
|
if (provider.selectedModels !== undefined && (
|
|
78
85
|
!Array.isArray(provider.selectedModels)
|
|
79
86
|
|| provider.selectedModels.some(model => typeof model !== "string")
|
|
@@ -81,9 +88,26 @@ function managedLegacyMultiOverlay(provider: OcxProviderConfig): Pick<OcxProvide
|
|
|
81
88
|
return {
|
|
82
89
|
...(provider.disabled !== undefined ? { disabled: provider.disabled } : {}),
|
|
83
90
|
...(provider.selectedModels !== undefined ? { selectedModels: [...provider.selectedModels] } : {}),
|
|
91
|
+
...(provider.modelCosts !== undefined ? { modelCosts: provider.modelCosts } : {}),
|
|
84
92
|
};
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
/** Shape check for a legacy overlay: a plain record of complete non-negative finite Cost4 rows. */
|
|
96
|
+
function validLegacyOverlayCosts(value: unknown): boolean {
|
|
97
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
98
|
+
const fields = ["input", "output", "cacheRead", "cacheWrite"] as const;
|
|
99
|
+
return Object.values(value as Record<string, unknown>).every(entry => {
|
|
100
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
101
|
+
const rates = entry as Record<string, unknown>;
|
|
102
|
+
return Object.keys(rates).length === fields.length
|
|
103
|
+
&& fields.every(key => Object.hasOwn(rates, key)
|
|
104
|
+
&& typeof rates[key] === "number"
|
|
105
|
+
&& Number.isFinite(rates[key])
|
|
106
|
+
&& (rates[key] as number) >= 0
|
|
107
|
+
&& (rates[key] as number) <= MAX_COST4_RATE);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
87
111
|
function rewriteLegacyOpenAiSelectedId(value: string): string {
|
|
88
112
|
return value.startsWith(LEGACY_OPENAI_MULTI_PREFIX)
|
|
89
113
|
? value.slice(LEGACY_OPENAI_MULTI_PREFIX.length)
|
|
@@ -95,6 +119,28 @@ function rewriteLegacyOpenAiModelList(values: string[] | undefined): string[] |
|
|
|
95
119
|
return [...new Set(values.map(rewriteLegacyOpenAiSelectedId))];
|
|
96
120
|
}
|
|
97
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Rewrite legacy `openai-multi/<model>` keys in a modelCosts overlay so the
|
|
124
|
+
* prices still match after the migration resolves logs as provider `openai`
|
|
125
|
+
* with the bare model id. Keys without the legacy prefix pass through.
|
|
126
|
+
*/
|
|
127
|
+
function rewriteLegacyOpenAiCostKeys(costs: Record<string, ProviderCostOverlay> | undefined): Record<string, ProviderCostOverlay> {
|
|
128
|
+
// Null-prototype map so prototype-named model ids (e.g. "__proto__") are
|
|
129
|
+
// stored as own properties instead of invoking the inherited setter.
|
|
130
|
+
const rewritten = Object.create(null) as Record<string, ProviderCostOverlay>;
|
|
131
|
+
if (costs) {
|
|
132
|
+
for (const [key, value] of Object.entries(costs)) {
|
|
133
|
+
const canonicalKey = rewriteLegacyOpenAiSelectedId(key);
|
|
134
|
+
// A bare <model> key always wins over its openai-multi/<model> equivalent
|
|
135
|
+
// inside the same row, regardless of JSON property order.
|
|
136
|
+
if (key === canonicalKey || !Object.hasOwn(rewritten, canonicalKey)) {
|
|
137
|
+
rewritten[canonicalKey] = value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return rewritten;
|
|
142
|
+
}
|
|
143
|
+
|
|
98
144
|
function mergeLegacyOpenAiProviderRows(
|
|
99
145
|
openai: OcxProviderConfig | undefined,
|
|
100
146
|
legacyMulti: OcxProviderConfig | undefined,
|
|
@@ -104,12 +150,22 @@ function mergeLegacyOpenAiProviderRows(
|
|
|
104
150
|
...(openai?.selectedModels ?? []),
|
|
105
151
|
...(legacyMulti?.selectedModels ?? []),
|
|
106
152
|
]);
|
|
153
|
+
// Both rows can carry disjoint overlays; merge them (canonical openai wins on
|
|
154
|
+
// key conflicts) so legacy Multi prices are not silently dropped. Keys are
|
|
155
|
+
// rewritten first so `openai-multi/<model>` entries still resolve after the
|
|
156
|
+
// provider is canonicalized to `openai`.
|
|
157
|
+
const modelCosts = {
|
|
158
|
+
...rewriteLegacyOpenAiCostKeys(legacyMulti?.modelCosts),
|
|
159
|
+
...rewriteLegacyOpenAiCostKeys(openai?.modelCosts),
|
|
160
|
+
};
|
|
161
|
+
const hasModelCosts = Object.keys(modelCosts).length > 0;
|
|
107
162
|
const formerRows = [openai, legacyMulti].filter((row): row is OcxProviderConfig => row !== undefined);
|
|
108
163
|
const disabled = formerRows.length > 0 && formerRows.every(row => row.disabled === true);
|
|
109
164
|
return {
|
|
110
165
|
...canonicalCodexForwardProvider(mode),
|
|
111
166
|
...(disabled ? { disabled: true } : {}),
|
|
112
167
|
...(selectedModels && selectedModels.length > 0 ? { selectedModels } : {}),
|
|
168
|
+
...(hasModelCosts ? { modelCosts } : {}),
|
|
113
169
|
};
|
|
114
170
|
}
|
|
115
171
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Zen short-window rate-limit guidance (#1145 / OCX-56).
|
|
3
|
+
*
|
|
4
|
+
* OpenCode's keyed and keyless Zen chat endpoints share `https://opencode.ai/zen/v1`.
|
|
5
|
+
* Free-model traffic can hit a short-window burst ceiling around 15–20 RPM
|
|
6
|
+
* (community-measured). Zen often answers with opaque `429 Rate limit exceeded`
|
|
7
|
+
* bodies and may omit `Retry-After` / `X-RateLimit-*`; when those headers are
|
|
8
|
+
* present they still take precedence. Distinct from the keyless desktop
|
|
9
|
+
* ~200 requests / 5h quota documented on `opencode-free`.
|
|
10
|
+
*/
|
|
11
|
+
import { validateClientRetryAfterHeader } from "../lib/retry-after";
|
|
12
|
+
import { registryEntryForProviderDestination } from "./registry";
|
|
13
|
+
|
|
14
|
+
const OPENCODE_ZEN_PROVIDER_IDS = new Set(["opencode-zen", "opencode-free"]);
|
|
15
|
+
|
|
16
|
+
/** Observed free-model burst ceiling on Zen (not an official OpenCode figure). */
|
|
17
|
+
export const OPENCODE_ZEN_OBSERVED_RPM_HINT = "roughly 15-20 requests per minute";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Synthetic client backoff when Zen omits Retry-After after a rate-limit 429.
|
|
21
|
+
* Longer than the generic 2s default so Codex-shaped clients do not immediately
|
|
22
|
+
* re-hammer a ~15-20 RPM window.
|
|
23
|
+
*/
|
|
24
|
+
export const OPENCODE_ZEN_SYNTHETIC_RETRY_AFTER_SEC = 15;
|
|
25
|
+
|
|
26
|
+
const ENRICHMENT_MARKER = "15-20 requests per minute";
|
|
27
|
+
|
|
28
|
+
export function isOpenCodeZenRateLimitProvider(opts: {
|
|
29
|
+
providerName?: string;
|
|
30
|
+
baseUrl?: string;
|
|
31
|
+
adapter?: string;
|
|
32
|
+
}): boolean {
|
|
33
|
+
const name = opts.providerName?.trim();
|
|
34
|
+
if (name && OPENCODE_ZEN_PROVIDER_IDS.has(name)) return true;
|
|
35
|
+
const baseUrl = opts.baseUrl?.trim();
|
|
36
|
+
if (!baseUrl) return false;
|
|
37
|
+
const entry = registryEntryForProviderDestination({
|
|
38
|
+
baseUrl,
|
|
39
|
+
adapter: opts.adapter?.trim() || "openai-chat",
|
|
40
|
+
authMode: "key",
|
|
41
|
+
});
|
|
42
|
+
return entry !== undefined && OPENCODE_ZEN_PROVIDER_IDS.has(entry.id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Same-key `retryOn429` only applies on key-authenticated HTTP paths — not
|
|
47
|
+
* keyless `opencode-free` traffic and not custom `runTurn` transports.
|
|
48
|
+
*/
|
|
49
|
+
export function supportsOpenCodeZenRetryOn429Guidance(opts: {
|
|
50
|
+
authMode?: string;
|
|
51
|
+
hasApiKey?: boolean;
|
|
52
|
+
supportsHttpSameKeyRetry?: boolean;
|
|
53
|
+
}): boolean {
|
|
54
|
+
if (opts.supportsHttpSameKeyRetry === false) return false;
|
|
55
|
+
if (opts.authMode !== undefined && opts.authMode !== "key") return false;
|
|
56
|
+
return opts.hasApiKey === true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Append actionable Zen rate-limit context to a generic upstream 429 message and
|
|
61
|
+
* embed a parseable `try again in Ns` hint so {@link resolveClientRetryAfter}
|
|
62
|
+
* surfaces a useful Retry-After when the gateway sent none.
|
|
63
|
+
*/
|
|
64
|
+
export function enrichOpenCodeZenRateLimitMessage(
|
|
65
|
+
message: string,
|
|
66
|
+
opts: {
|
|
67
|
+
status: number;
|
|
68
|
+
providerName?: string;
|
|
69
|
+
baseUrl?: string;
|
|
70
|
+
adapter?: string;
|
|
71
|
+
authMode?: string;
|
|
72
|
+
hasApiKey?: boolean;
|
|
73
|
+
/** Upstream Retry-After header; when valid, skip the synthetic 15s text hint. */
|
|
74
|
+
upstreamRetryAfter?: string | null;
|
|
75
|
+
/** False for custom `runTurn` transports outside the HTTP retry loop. */
|
|
76
|
+
supportsHttpSameKeyRetry?: boolean;
|
|
77
|
+
now?: number;
|
|
78
|
+
},
|
|
79
|
+
): string {
|
|
80
|
+
if (opts.status !== 429) return message;
|
|
81
|
+
if (!isOpenCodeZenRateLimitProvider(opts)) return message;
|
|
82
|
+
if (!/rate\s*limit/i.test(message)) return message;
|
|
83
|
+
if (message.includes(ENRICHMENT_MARKER)) return message;
|
|
84
|
+
|
|
85
|
+
const upstreamRetry = validateClientRetryAfterHeader(
|
|
86
|
+
opts.upstreamRetryAfter,
|
|
87
|
+
opts.now ?? Date.now(),
|
|
88
|
+
);
|
|
89
|
+
const retryHint = upstreamRetry || /try again in \d/i.test(message)
|
|
90
|
+
? ""
|
|
91
|
+
: ` Try again in ${OPENCODE_ZEN_SYNTHETIC_RETRY_AFTER_SEC}s.`;
|
|
92
|
+
const paceHint = supportsOpenCodeZenRetryOn429Guidance(opts)
|
|
93
|
+
? " Slow the request pace, or set providers.opencode-zen.retryOn429 for same-key backoff."
|
|
94
|
+
: " Slow the request pace.";
|
|
95
|
+
return (
|
|
96
|
+
`${message}`
|
|
97
|
+
+ ` OpenCode Zen free-model traffic is often limited to ${OPENCODE_ZEN_OBSERVED_RPM_HINT}`
|
|
98
|
+
+ ` (observed; OpenCode does not publish this RPM, and may omit rate-limit headers).`
|
|
99
|
+
+ `${retryHint}`
|
|
100
|
+
+ paceHint
|
|
101
|
+
);
|
|
102
|
+
}
|