@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/providers/quota.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "../codex/auth-api";
|
|
7
7
|
import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache";
|
|
8
8
|
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
9
|
+
import { codexPlanKey } from "../codex/plan";
|
|
9
10
|
import { resolveEnvValue } from "../config";
|
|
10
11
|
import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
|
|
11
12
|
import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
sweepExpiredOnWrite,
|
|
21
22
|
type GenerationContext,
|
|
22
23
|
} from "../lib/state-store-sweeper";
|
|
24
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
23
25
|
import {
|
|
24
26
|
aggregateCodexPoolCapacity,
|
|
25
27
|
CODEX_CAPACITY_MAX_QUOTA_AGE_MS,
|
|
@@ -32,6 +34,8 @@ const ACCOUNT_TOKEN_SKEW_MS = 60_000;
|
|
|
32
34
|
|
|
33
35
|
const CACHE_TTL_MS = 5 * 60_000;
|
|
34
36
|
const REQUEST_TIMEOUT_MS = 8_000;
|
|
37
|
+
/** Successful provider quota payloads are small; reject oversized or stalled JSON before parsing. */
|
|
38
|
+
export const QUOTA_RESPONSE_MAX_BYTES = 512 * 1024;
|
|
35
39
|
const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
36
40
|
const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
|
|
37
41
|
const A6API_BASE_URL = "https://api.a6api.com";
|
|
@@ -168,7 +172,7 @@ function cacheKeyWithAggregationState(
|
|
|
168
172
|
const rows = snapshot.accounts.map(account => ({
|
|
169
173
|
isMain: account.isMain,
|
|
170
174
|
active: account.id === activeId,
|
|
171
|
-
plan: account.plan
|
|
175
|
+
plan: codexPlanKey(account.plan) ?? null,
|
|
172
176
|
paused: account.paused,
|
|
173
177
|
needsReauth: account.needsReauth === true,
|
|
174
178
|
quota: quotaSignatureValue(account.quota as CodexCapacityQuota | null),
|
|
@@ -259,6 +263,43 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
259
263
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
260
264
|
}
|
|
261
265
|
|
|
266
|
+
const QUOTA_JSON_READ_FAILURE = Symbol("quota-json-read-failure");
|
|
267
|
+
|
|
268
|
+
async function readQuotaJson(
|
|
269
|
+
response: Response,
|
|
270
|
+
timeoutMs = REQUEST_TIMEOUT_MS,
|
|
271
|
+
): Promise<unknown | typeof QUOTA_JSON_READ_FAILURE> {
|
|
272
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
273
|
+
if (Number.isFinite(declaredLength) && declaredLength > QUOTA_RESPONSE_MAX_BYTES) {
|
|
274
|
+
try {
|
|
275
|
+
void response.body?.cancel(
|
|
276
|
+
new DOMException("Provider quota response is too large", "QuotaExceededError"),
|
|
277
|
+
).catch(() => undefined);
|
|
278
|
+
} catch {
|
|
279
|
+
// Best-effort cancellation only.
|
|
280
|
+
}
|
|
281
|
+
return QUOTA_JSON_READ_FAILURE;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const bounded = await readBoundedResponseBody(response, {
|
|
286
|
+
maxBytes: QUOTA_RESPONSE_MAX_BYTES,
|
|
287
|
+
totalTimeoutMs: timeoutMs,
|
|
288
|
+
inactivityTimeoutMs: timeoutMs,
|
|
289
|
+
});
|
|
290
|
+
if (bounded.oversized || bounded.truncated || !bounded.displaySafe) return QUOTA_JSON_READ_FAILURE;
|
|
291
|
+
return JSON.parse(bounded.text) as unknown;
|
|
292
|
+
} catch {
|
|
293
|
+
return QUOTA_JSON_READ_FAILURE;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Test-only access to the quota reader's deadline and cancellation contract. */
|
|
298
|
+
export async function readProviderQuotaJsonForTests(response: Response, timeoutMs: number): Promise<unknown> {
|
|
299
|
+
const result = await readQuotaJson(response, timeoutMs);
|
|
300
|
+
return result === QUOTA_JSON_READ_FAILURE ? null : result;
|
|
301
|
+
}
|
|
302
|
+
|
|
262
303
|
function isBuiltInChatGptForwardProvider(name: string, provider: OcxProviderConfig): boolean {
|
|
263
304
|
return name === OPENAI_CODEX_PROVIDER_ID && isCanonicalOpenAiForwardProvider(provider);
|
|
264
305
|
}
|
|
@@ -353,8 +394,13 @@ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Pro
|
|
|
353
394
|
? TERMINAL_QUOTA_FAILURE
|
|
354
395
|
: null;
|
|
355
396
|
}
|
|
356
|
-
const
|
|
357
|
-
|
|
397
|
+
const [subscriptionBody, tokenBody] = await Promise.all([
|
|
398
|
+
readQuotaJson(subscriptionResponse),
|
|
399
|
+
readQuotaJson(tokenResponse),
|
|
400
|
+
]);
|
|
401
|
+
if (subscriptionBody === QUOTA_JSON_READ_FAILURE || tokenBody === QUOTA_JSON_READ_FAILURE) return null;
|
|
402
|
+
const subscription = a6apiPayload(subscriptionBody);
|
|
403
|
+
const token = a6apiPayload(tokenBody);
|
|
358
404
|
const unlimited = token?.unlimited_quota === true
|
|
359
405
|
|| token?.unlimited_quota === 1
|
|
360
406
|
|| token?.unlimited_quota === "true";
|
|
@@ -432,7 +478,7 @@ async function fetchOpenRouterQuota(provider: string, config: OcxProviderConfig)
|
|
|
432
478
|
? TERMINAL_QUOTA_FAILURE
|
|
433
479
|
: null;
|
|
434
480
|
}
|
|
435
|
-
const body = asRecord(await response
|
|
481
|
+
const body = asRecord(await readQuotaJson(response));
|
|
436
482
|
const data = asRecord(body?.data) ?? body;
|
|
437
483
|
if (!data) return null;
|
|
438
484
|
const limit = toFiniteNumber(data.limit);
|
|
@@ -479,7 +525,7 @@ async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig):
|
|
|
479
525
|
? TERMINAL_QUOTA_FAILURE
|
|
480
526
|
: null;
|
|
481
527
|
}
|
|
482
|
-
const body = asRecord(await response
|
|
528
|
+
const body = asRecord(await readQuotaJson(response));
|
|
483
529
|
// The payload nests balances under `balance_infos` rows keyed by currency;
|
|
484
530
|
// prefer a USD row, then CNY, then the first row that parses.
|
|
485
531
|
const infos = Array.isArray(body?.balance_infos) ? body.balance_infos as unknown[] : null;
|
|
@@ -527,7 +573,7 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
|
|
|
527
573
|
? TERMINAL_QUOTA_FAILURE
|
|
528
574
|
: null;
|
|
529
575
|
}
|
|
530
|
-
const body = asRecord(await response
|
|
576
|
+
const body = asRecord(await readQuotaJson(response));
|
|
531
577
|
const data = asRecord(body?.data) ?? body;
|
|
532
578
|
const limits = Array.isArray(data?.limits) ? data.limits : null;
|
|
533
579
|
if (!limits) return null;
|
|
@@ -575,7 +621,7 @@ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promi
|
|
|
575
621
|
? TERMINAL_QUOTA_FAILURE
|
|
576
622
|
: null;
|
|
577
623
|
}
|
|
578
|
-
const body = asRecord(await response
|
|
624
|
+
const body = asRecord(await readQuotaJson(response));
|
|
579
625
|
if (!body || body.success === false) return null;
|
|
580
626
|
const data = asRecord(body.data) ?? body;
|
|
581
627
|
// The plugin renders a 5h token window, a weekly window, and a monthly MCP
|
|
@@ -631,7 +677,7 @@ async function fetchMinimaxQuota(provider: string, config: OcxProviderConfig): P
|
|
|
631
677
|
? TERMINAL_QUOTA_FAILURE
|
|
632
678
|
: null;
|
|
633
679
|
}
|
|
634
|
-
const body = asRecord(await response
|
|
680
|
+
const body = asRecord(await readQuotaJson(response));
|
|
635
681
|
if (!body || body.success === false) return null;
|
|
636
682
|
const data = asRecord(body.data) ?? body;
|
|
637
683
|
const remainsMs = toFiniteNumber(data.remains_time ?? data.remainsTime);
|
|
@@ -674,7 +720,7 @@ async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig):
|
|
|
674
720
|
? TERMINAL_QUOTA_FAILURE
|
|
675
721
|
: null;
|
|
676
722
|
}
|
|
677
|
-
const body = asRecord(await response
|
|
723
|
+
const body = asRecord(await readQuotaJson(response));
|
|
678
724
|
const data = asRecord(body?.data) ?? body;
|
|
679
725
|
if (!data) return null;
|
|
680
726
|
const available = toFiniteNumber(data.available_balance);
|
|
@@ -710,7 +756,7 @@ async function fetchVeniceQuota(provider: string, config: OcxProviderConfig): Pr
|
|
|
710
756
|
? TERMINAL_QUOTA_FAILURE
|
|
711
757
|
: null;
|
|
712
758
|
}
|
|
713
|
-
const body = asRecord(await response
|
|
759
|
+
const body = asRecord(await readQuotaJson(response));
|
|
714
760
|
const data = asRecord(body?.data) ?? body;
|
|
715
761
|
if (!data) return null;
|
|
716
762
|
const diemBalance = toFiniteNumber(data.balance);
|
|
@@ -753,7 +799,7 @@ async function fetchSyntheticQuota(provider: string, config: OcxProviderConfig):
|
|
|
753
799
|
? TERMINAL_QUOTA_FAILURE
|
|
754
800
|
: null;
|
|
755
801
|
}
|
|
756
|
-
const body = asRecord(await response
|
|
802
|
+
const body = asRecord(await readQuotaJson(response));
|
|
757
803
|
const data = asRecord(body?.data) ?? body;
|
|
758
804
|
const quota: ProviderQuota = { updatedAt: Date.now() };
|
|
759
805
|
let windows = 0;
|
|
@@ -801,7 +847,7 @@ async function fetchDeepInfraQuota(provider: string, config: OcxProviderConfig):
|
|
|
801
847
|
? TERMINAL_QUOTA_FAILURE
|
|
802
848
|
: null;
|
|
803
849
|
}
|
|
804
|
-
const body = asRecord(await response
|
|
850
|
+
const body = asRecord(await readQuotaJson(response));
|
|
805
851
|
const data = asRecord(body?.data) ?? body;
|
|
806
852
|
if (!data) return null;
|
|
807
853
|
const stripeBalance = toFiniteNumber(data.stripe_balance);
|
|
@@ -843,7 +889,7 @@ async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig)
|
|
|
843
889
|
? TERMINAL_QUOTA_FAILURE
|
|
844
890
|
: null;
|
|
845
891
|
}
|
|
846
|
-
const body = asRecord(await response
|
|
892
|
+
const body = asRecord(await readQuotaJson(response));
|
|
847
893
|
const data = asRecord(body?.data) ?? body;
|
|
848
894
|
const quota: ProviderQuota = { updatedAt: Date.now() };
|
|
849
895
|
let windows = 0;
|
|
@@ -1027,7 +1073,7 @@ async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promi
|
|
|
1027
1073
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1028
1074
|
});
|
|
1029
1075
|
if (!response.ok) return null;
|
|
1030
|
-
const parsed = parseXaiCreditsResponse(await response
|
|
1076
|
+
const parsed = parseXaiCreditsResponse(await readQuotaJson(response));
|
|
1031
1077
|
if (!parsed) return null;
|
|
1032
1078
|
return {
|
|
1033
1079
|
weeklyPercent: parsed.percent,
|
|
@@ -1061,7 +1107,7 @@ async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | nu
|
|
|
1061
1107
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1062
1108
|
});
|
|
1063
1109
|
if (!response.ok) return null;
|
|
1064
|
-
const body = asRecord(await response
|
|
1110
|
+
const body = asRecord(await readQuotaJson(response));
|
|
1065
1111
|
const config = asRecord(body?.config);
|
|
1066
1112
|
if (!config) return null;
|
|
1067
1113
|
const limitCents = centsValue(config.monthlyLimit);
|
|
@@ -1107,7 +1153,7 @@ async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQu
|
|
|
1107
1153
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1108
1154
|
});
|
|
1109
1155
|
if (!response.ok) return null;
|
|
1110
|
-
const body = asRecord(await response
|
|
1156
|
+
const body = asRecord(await readQuotaJson(response));
|
|
1111
1157
|
if (!body) return null;
|
|
1112
1158
|
const fiveHour = parseClaudeBucket(body.five_hour);
|
|
1113
1159
|
const sevenDay = parseClaudeBucket(body.seven_day);
|
|
@@ -1520,7 +1566,7 @@ async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Prom
|
|
|
1520
1566
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1521
1567
|
});
|
|
1522
1568
|
if (!response.ok) return null;
|
|
1523
|
-
const quota = parseKimiQuotaPayload(await response
|
|
1569
|
+
const quota = parseKimiQuotaPayload(await readQuotaJson(response));
|
|
1524
1570
|
return quota ? report(provider, "kimi:usages", quota) : null;
|
|
1525
1571
|
}
|
|
1526
1572
|
|
|
@@ -1553,7 +1599,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
|
|
|
1553
1599
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1554
1600
|
});
|
|
1555
1601
|
if (periodRes.ok) {
|
|
1556
|
-
const body = asRecord(await periodRes
|
|
1602
|
+
const body = asRecord(await readQuotaJson(periodRes));
|
|
1557
1603
|
const planUsage = asRecord(body?.planUsage);
|
|
1558
1604
|
if (planUsage) {
|
|
1559
1605
|
const resetAt = normalizeResetAt(body?.billingCycleEnd ?? planUsage.billingCycleEnd ?? body?.periodEnd);
|
|
@@ -1615,7 +1661,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
|
|
|
1615
1661
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1616
1662
|
});
|
|
1617
1663
|
if (summaryRes.ok) {
|
|
1618
|
-
const body = asRecord(await summaryRes
|
|
1664
|
+
const body = asRecord(await readQuotaJson(summaryRes));
|
|
1619
1665
|
const individual = asRecord(body?.individualUsage);
|
|
1620
1666
|
const plan = asRecord(individual?.plan);
|
|
1621
1667
|
if (plan) {
|
|
@@ -1644,7 +1690,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
|
|
|
1644
1690
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1645
1691
|
});
|
|
1646
1692
|
if (!response.ok) return null;
|
|
1647
|
-
const body = asRecord(await response
|
|
1693
|
+
const body = asRecord(await readQuotaJson(response));
|
|
1648
1694
|
if (!body) return null;
|
|
1649
1695
|
|
|
1650
1696
|
// Prefer the gpt-4 bucket (historical "fast requests"); else first model with used+limit.
|
|
@@ -1758,7 +1804,7 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
|
|
|
1758
1804
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1759
1805
|
});
|
|
1760
1806
|
if (!response.ok) return null;
|
|
1761
|
-
const body = asRecord(await response
|
|
1807
|
+
const body = asRecord(await readQuotaJson(response));
|
|
1762
1808
|
const models = asRecord(body?.models);
|
|
1763
1809
|
if (!models) return null;
|
|
1764
1810
|
|
|
@@ -186,6 +186,11 @@ export interface ProviderRegistryEntry {
|
|
|
186
186
|
* replay miss are repaired rather than forwarded.
|
|
187
187
|
*/
|
|
188
188
|
statelessResponses?: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Responses parser requires a matched tool result directly after its call. This is
|
|
191
|
+
* seeded/backfilled like other fixed upstream wire-contract capabilities.
|
|
192
|
+
*/
|
|
193
|
+
requiresAdjacentResponsesToolResults?: boolean;
|
|
189
194
|
/**
|
|
190
195
|
* Registry default for the provider's Responses `service_tier` support; see
|
|
191
196
|
* `OcxProviderConfig.supportsServiceTier`. Registry-only: backfilled (never
|
|
@@ -209,6 +214,12 @@ export interface ProviderRegistryEntry {
|
|
|
209
214
|
modelDefaultReasoningEfforts?: Record<string, string>;
|
|
210
215
|
reasoningEffortMap?: Record<string, string>;
|
|
211
216
|
modelReasoningEffortMap?: Record<string, Record<string, string>>;
|
|
217
|
+
/**
|
|
218
|
+
* Registry-authoritative models that send OpenAI's direct `reasoning_effort` field.
|
|
219
|
+
* Runtime enrichment uses this to repair stale preset metadata that still classifies a model
|
|
220
|
+
* as a thinking-budget/toggle model. This is registry-only and is never persisted as user config.
|
|
221
|
+
*/
|
|
222
|
+
directReasoningEffortModels?: string[];
|
|
212
223
|
reasoningWireFormat?: OcxProviderConfig["reasoningWireFormat"];
|
|
213
224
|
noVisionModels?: string[];
|
|
214
225
|
noReasoningModels?: string[];
|
|
@@ -221,6 +232,7 @@ export interface ProviderRegistryEntry {
|
|
|
221
232
|
promptCacheKey?: boolean;
|
|
222
233
|
autoToolChoiceOnlyModels?: string[];
|
|
223
234
|
preserveReasoningContentModels?: string[];
|
|
235
|
+
requiresReasoningPlaceholderModels?: string[];
|
|
224
236
|
reasoningSplitModels?: string[];
|
|
225
237
|
thinkingToggleModels?: string[];
|
|
226
238
|
thinkingBudgetModels?: string[];
|
|
@@ -243,7 +255,7 @@ export type ProviderConfigSeed = Pick<
|
|
|
243
255
|
| "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens"
|
|
244
256
|
| "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "reasoningWireFormat"
|
|
245
257
|
| "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
|
|
246
|
-
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "reasoningSplitModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames"
|
|
258
|
+
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames"
|
|
247
259
|
| "googleMode" | "project" | "location" | "headers"
|
|
248
260
|
>;
|
|
249
261
|
|
|
@@ -344,6 +356,9 @@ const ZHIPU_BIGMODEL_INPUT_MODALITIES: Record<string, string[]> = {
|
|
|
344
356
|
};
|
|
345
357
|
const ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS = ["glm-4.6", "glm-4.7", "glm-5", "glm-5.1"];
|
|
346
358
|
const THINKING_BUDGET_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
359
|
+
// Qwen3.8-Max is the first Qwen3.x model with official direct `reasoning_effort` support.
|
|
360
|
+
// Evidence: https://qwen.ai/blog?id=qwen3.8
|
|
361
|
+
const QWEN38_REASONING_EFFORTS = ["low", "medium", "xhigh"];
|
|
347
362
|
const THINKING_BUDGET_MODELS = [
|
|
348
363
|
"qwen3.5-397b", "qwen3.6-35b",
|
|
349
364
|
"qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus",
|
|
@@ -1359,6 +1374,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1359
1374
|
// "The API is stateless: responses and conversations are not stored on the
|
|
1360
1375
|
// server." https://api-docs.deepseek.com/api/create-response/
|
|
1361
1376
|
statelessResponses: true,
|
|
1377
|
+
// DeepSeek rejects a valid Codex continuation when hook-provided developer
|
|
1378
|
+
// context is persisted between a call and its matching result (#1292).
|
|
1379
|
+
requiresAdjacentResponsesToolResults: true,
|
|
1362
1380
|
/* [Decision Log]
|
|
1363
1381
|
- 목적: DeepSeek V4 thinking mode multi-turn/tool-call requests must replay prior assistant reasoning_content.
|
|
1364
1382
|
- 대안 분석: Globally preserve reasoning_content for all OpenAI-compatible models; preserve it for legacy deepseek-reasoner too; mark only V4 thinking models in registry metadata.
|
|
@@ -1375,6 +1393,40 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1375
1393
|
},
|
|
1376
1394
|
// llama-3.3-70b was deprecated by Cerebras on 2026-02-16. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
|
|
1377
1395
|
{ id: "cerebras", label: "Cerebras", baseUrl: "https://api.cerebras.ai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://cloud.cerebras.ai/platform/apikeys", defaultModel: "gpt-oss-120b" },
|
|
1396
|
+
{
|
|
1397
|
+
// Primary sources checked 2026-08-08:
|
|
1398
|
+
// - https://chutes.ai/pricing documents the shared llm.chutes.ai/v1 OpenAI-compatible
|
|
1399
|
+
// gateway, Bearer API keys, and chat completions. Its public
|
|
1400
|
+
// https://llm.chutes.ai/v1/models response supplies supported_features for filtering.
|
|
1401
|
+
// - https://chutes.ai/terms identifies Chutes Global Corp as the platform operator, applies
|
|
1402
|
+
// to API consumers, and directs production/high-volume automated inference to PAYGO.
|
|
1403
|
+
// Maintainer: @olddonkey; no affiliation with Chutes.
|
|
1404
|
+
id: "chutes",
|
|
1405
|
+
label: "Chutes",
|
|
1406
|
+
baseUrl: "https://llm.chutes.ai/v1",
|
|
1407
|
+
adapter: "openai-chat",
|
|
1408
|
+
authKind: "key",
|
|
1409
|
+
dashboardUrl: "https://chutes.ai/auth/start",
|
|
1410
|
+
liveModels: true,
|
|
1411
|
+
preserveCustomDestination: true,
|
|
1412
|
+
// The public model catalog cannot prove that a supplied Bearer key is valid.
|
|
1413
|
+
apiKeyValidation: "unknown",
|
|
1414
|
+
// Chutes documents tool calling, but not a provider-wide parallel tool-call contract.
|
|
1415
|
+
parallelToolCalls: false,
|
|
1416
|
+
// The live catalog reports reasoning support, but not a stable effort ladder.
|
|
1417
|
+
reasoningEfforts: [],
|
|
1418
|
+
modelDiscovery: {
|
|
1419
|
+
path: "models",
|
|
1420
|
+
maxResponseBytes: 256 * 1024,
|
|
1421
|
+
maxModels: 128,
|
|
1422
|
+
filter: {
|
|
1423
|
+
// The shared LLM catalog also contains rows without native tool support. Codex needs a
|
|
1424
|
+
// complete agent loop, so admit only rows whose live metadata advertises tools.
|
|
1425
|
+
allOf: [{ path: ["supported_features"], containsAny: ["tools"] }],
|
|
1426
|
+
},
|
|
1427
|
+
},
|
|
1428
|
+
note: "Shared OpenAI-compatible LLM gateway only; live discovery exposes tool-capable rows. User-deployed custom Chute endpoints and non-LLM APIs require a custom provider.",
|
|
1429
|
+
},
|
|
1378
1430
|
{
|
|
1379
1431
|
id: "deepinfra",
|
|
1380
1432
|
label: "DeepInfra",
|
|
@@ -1621,6 +1673,53 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1621
1673
|
},
|
|
1622
1674
|
note: "Shared Generative APIs Serverless Chat Completions only; project-qualified and dedicated deployment hosts require a custom provider.",
|
|
1623
1675
|
},
|
|
1676
|
+
{
|
|
1677
|
+
// Primary sources checked 2026-08-08:
|
|
1678
|
+
// - https://featherless.ai/docs/api-overview-and-common-options documents the fixed
|
|
1679
|
+
// OpenAI-compatible base URL, Bearer keys, and Chat Completions.
|
|
1680
|
+
// - https://featherless.ai/docs/api-reference-models documents authenticated plan filtering,
|
|
1681
|
+
// chat capability filtering, popularity sorting, pagination, and per-row tool metadata.
|
|
1682
|
+
// - https://featherless.ai/legal/terms-of-service identifies Featherless as a Delaware LLC,
|
|
1683
|
+
// covers developers building on its APIs, and reserves arbitrary applications for Scale
|
|
1684
|
+
// plans. Maintainer: @olddonkey; no affiliation with Featherless.
|
|
1685
|
+
id: "featherless",
|
|
1686
|
+
label: "Featherless AI",
|
|
1687
|
+
baseUrl: "https://api.featherless.ai/v1",
|
|
1688
|
+
adapter: "openai-chat",
|
|
1689
|
+
authKind: "key",
|
|
1690
|
+
dashboardUrl: "https://featherless.ai/account/api-keys",
|
|
1691
|
+
liveModels: true,
|
|
1692
|
+
preserveCustomDestination: true,
|
|
1693
|
+
// /v1/models is documented as callable authenticated or unauthenticated, so a 2xx catalog
|
|
1694
|
+
// response cannot prove the supplied Bearer key is valid.
|
|
1695
|
+
apiKeyValidation: "unknown",
|
|
1696
|
+
// Featherless documents tool calling, but not a provider-wide parallel tool-call contract.
|
|
1697
|
+
parallelToolCalls: false,
|
|
1698
|
+
// Reasoning controls use model-specific chat_template_kwargs, not OpenAI reasoning_effort.
|
|
1699
|
+
reasoningEfforts: [],
|
|
1700
|
+
modelDiscovery: {
|
|
1701
|
+
path: "models",
|
|
1702
|
+
query: {
|
|
1703
|
+
available_on_current_plan: "true",
|
|
1704
|
+
capabilities: "chat",
|
|
1705
|
+
page: "1",
|
|
1706
|
+
per_page: "100",
|
|
1707
|
+
sort: "-popularity",
|
|
1708
|
+
},
|
|
1709
|
+
maxResponseBytes: 128 * 1024,
|
|
1710
|
+
maxModels: 100,
|
|
1711
|
+
filter: {
|
|
1712
|
+
// Treat server-side filters as a size optimization, not an authority boundary. A row must
|
|
1713
|
+
// independently prove plan availability, no separate Hugging Face gate, and tool support.
|
|
1714
|
+
allOf: [
|
|
1715
|
+
{ path: ["available_on_current_plan"], equalsAny: [true] },
|
|
1716
|
+
{ path: ["is_gated"], equalsAny: [false] },
|
|
1717
|
+
{ path: ["features", "tool_use"], equalsAny: [true] },
|
|
1718
|
+
],
|
|
1719
|
+
},
|
|
1720
|
+
},
|
|
1721
|
+
note: "Authenticated first page of popular chat models only; live discovery admits at most 100 plan-available, ungated rows whose metadata explicitly reports tool use.",
|
|
1722
|
+
},
|
|
1624
1723
|
// FREEZE 2026-07-10: exact serverless ids remain auth-gated/unverified. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
|
|
1625
1724
|
{ id: "together", label: "Together", baseUrl: "https://api.together.xyz/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://api.together.xyz/settings/api-keys" },
|
|
1626
1725
|
{ id: "fireworks", label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://fireworks.ai/account/api-keys" },
|
|
@@ -1721,6 +1820,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1721
1820
|
ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS.map(id => [id, true]),
|
|
1722
1821
|
),
|
|
1723
1822
|
preserveReasoningContentModels: ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS,
|
|
1823
|
+
// GLM thinking is a binary toggle (low maps to disabled), so a legitimate
|
|
1824
|
+
// tool round can carry no reasoning at all; never fabricate a placeholder
|
|
1825
|
+
// for it, only replay real recorded text (P2 on #1205).
|
|
1826
|
+
requiresReasoningPlaceholderModels: [],
|
|
1724
1827
|
// No liveModels: GET /api/paas/v4/models has not been observed to answer on this host, and a
|
|
1725
1828
|
// false live claim yields an empty picker at runtime. Flip it on once someone verifies it.
|
|
1726
1829
|
note: "Domestic BigModel pay-as-you-go endpoint (open.bigmodel.cn)",
|
|
@@ -1899,11 +2002,14 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1899
2002
|
},
|
|
1900
2003
|
modelReasoningEfforts: {
|
|
1901
2004
|
...Object.fromEntries(ALIBABA_TOKEN_PLAN_QWEN_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
|
|
2005
|
+
"qwen3.8-max": QWEN38_REASONING_EFFORTS,
|
|
1902
2006
|
"glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
|
|
1903
2007
|
"deepseek-v4-pro": deepseekThinkingEffortsFor("deepseek-v4-pro"),
|
|
1904
2008
|
},
|
|
2009
|
+
modelDefaultReasoningEfforts: { "qwen3.8-max": "xhigh" },
|
|
1905
2010
|
modelReasoningEffortMap: { "deepseek-v4-pro": deepseekReasoningMapFor("deepseek-v4-pro") },
|
|
1906
|
-
|
|
2011
|
+
directReasoningEffortModels: ["qwen3.8-max"],
|
|
2012
|
+
thinkingBudgetModels: ALIBABA_TOKEN_PLAN_QWEN_MODELS.filter(id => id !== "qwen3.8-max"),
|
|
1907
2013
|
preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash"],
|
|
1908
2014
|
noVisionModels: ["glm-5.2", "deepseek-v4-pro"],
|
|
1909
2015
|
},
|
|
@@ -1932,7 +2038,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1932
2038
|
},
|
|
1933
2039
|
modelReasoningEfforts: {
|
|
1934
2040
|
...Object.fromEntries(ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
|
|
1935
|
-
"qwen3.8-max":
|
|
2041
|
+
"qwen3.8-max": QWEN38_REASONING_EFFORTS,
|
|
1936
2042
|
"glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
|
|
1937
2043
|
"deepseek-v4-pro": deepseekThinkingEffortsFor("deepseek-v4-pro"),
|
|
1938
2044
|
"deepseek-v4-flash": deepseekThinkingEffortsFor("deepseek-v4-flash"),
|
|
@@ -1941,7 +2047,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1941
2047
|
"deepseek-v4-pro": deepseekReasoningMapFor("deepseek-v4-pro"),
|
|
1942
2048
|
"deepseek-v4-flash": deepseekReasoningMapFor("deepseek-v4-flash"),
|
|
1943
2049
|
},
|
|
1944
|
-
|
|
2050
|
+
directReasoningEffortModels: ["qwen3.8-max"],
|
|
2051
|
+
thinkingBudgetModels: ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS.filter(id => id !== "qwen3.8-max"),
|
|
1945
2052
|
preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "deepseek-v4-flash", "qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash"],
|
|
1946
2053
|
noVisionModels: ["deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v3.2", "glm-5.2", "glm-5.1", "glm-5", "MiniMax-M2.5"],
|
|
1947
2054
|
noReasoningModels: ["kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5", "deepseek-v3.2", "glm-5.1", "glm-5", "MiniMax-M2.5"],
|
|
@@ -1995,6 +2102,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1995
2102
|
modelDefaultReasoningEfforts: { "MiniMax-M3": "medium" },
|
|
1996
2103
|
modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP },
|
|
1997
2104
|
preserveReasoningContentModels: MINIMAX_MODELS,
|
|
2105
|
+
// MiniMax-M3 low effort maps to thinking disabled, so a legitimate tool
|
|
2106
|
+
// round can carry no reasoning at all; only replay real recorded text,
|
|
2107
|
+
// never a fabricated placeholder (chatgpt-codex-connector P2 on #1205).
|
|
2108
|
+
requiresReasoningPlaceholderModels: [],
|
|
1998
2109
|
reasoningSplitModels: MINIMAX_MODELS,
|
|
1999
2110
|
thinkingToggleModels: ["MiniMax-M3"],
|
|
2000
2111
|
jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive", note: "Subscription Key or API Key",
|
|
@@ -2007,6 +2118,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2007
2118
|
modelDefaultReasoningEfforts: { "MiniMax-M3": "medium" },
|
|
2008
2119
|
modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP },
|
|
2009
2120
|
preserveReasoningContentModels: MINIMAX_MODELS,
|
|
2121
|
+
requiresReasoningPlaceholderModels: [],
|
|
2010
2122
|
reasoningSplitModels: MINIMAX_MODELS,
|
|
2011
2123
|
thinkingToggleModels: ["MiniMax-M3"],
|
|
2012
2124
|
jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive", note: "中国区 Subscription Key",
|
|
@@ -2037,6 +2149,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2037
2149
|
// continuations, or the gateway answers HTTP 400 (issues #950/#994). Mirror the DeepSeek
|
|
2038
2150
|
// reasoning + thinking metadata so `opencode-zen/deepseek-v4-flash-free` — and the other
|
|
2039
2151
|
// Zen DeepSeek thinking models — never serialize a bare tool-call turn.
|
|
2152
|
+
note: "Keyed OpenCode Zen gateway. Free models on this tier are often short-window rate-limited at roughly 15-20 requests/minute (community-measured; OpenCode does not publish RPM). Zen may return generic 429s without Retry-After / X-RateLimit headers; when Retry-After is omitted, opencodex adds a synthetic backoff hint (upstream Retry-After still wins). Distinct from the keyless opencode-free desktop quota (~200 Big Pickle/free-model requests per 5 hours). Docs: https://opencode.ai/docs/zen/. Free-model prompts may be retained for training — do not send confidential material.",
|
|
2040
2153
|
modelReasoningEfforts: Object.fromEntries(
|
|
2041
2154
|
[...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS].map(id => [id, deepseekThinkingEffortsFor(id)]),
|
|
2042
2155
|
),
|
|
@@ -2056,7 +2169,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2056
2169
|
keyOptional: true,
|
|
2057
2170
|
featured: true,
|
|
2058
2171
|
liveModels: true,
|
|
2059
|
-
note: "No key needed — public desktop tier. OpenCode currently advertises about 200 Big Pickle/free-model requests per 5 hours. Free models are discovered live from Zen. Data use: per OpenCode's Zen docs (https://opencode.ai/docs/zen/), prompts sent to free models may be retained and used for training/improvement — do not send confidential material through this provider.",
|
|
2172
|
+
note: "No key needed — public desktop tier. OpenCode currently advertises about 200 Big Pickle/free-model requests per 5 hours. The same Zen gateway can also short-window rate-limit free models at roughly 15-20 requests/minute, and may return generic 429s without Retry-After (opencodex synthesizes backoff only when that header is omitted). Free models are discovered live from Zen. Data use: per OpenCode's Zen docs (https://opencode.ai/docs/zen/), prompts sent to free models may be retained and used for training/improvement — do not send confidential material through this provider.",
|
|
2060
2173
|
dashboardUrl: "https://opencode.ai",
|
|
2061
2174
|
staticHeaders: {
|
|
2062
2175
|
"x-opencode-client": "desktop",
|
package/src/router.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { hasOwnProvider, resolveEnvValue } from "./config";
|
|
|
12
12
|
import { assertProviderDestinationAllowed } from "./lib/destination-policy";
|
|
13
13
|
import { redactSecretString, redactUrlForLog } from "./lib/redact";
|
|
14
14
|
import { PROVIDER_REGISTRY, providerCodexAccountMode, providerMatchesRegistryTransport } from "./providers/registry";
|
|
15
|
+
import { applyDirectReasoningEffortContracts } from "./providers/derive";
|
|
15
16
|
import {
|
|
16
17
|
isCanonicalOpenAiForwardProvider,
|
|
17
18
|
LEGACY_CHATGPT_PROVIDER_ID,
|
|
@@ -284,6 +285,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
284
285
|
const noPenaltyModels = mergeStringArray(registryEntry.noPenaltyModels, provider.noPenaltyModels);
|
|
285
286
|
const autoToolChoiceOnlyModels = mergeStringArray(registryEntry.autoToolChoiceOnlyModels, provider.autoToolChoiceOnlyModels);
|
|
286
287
|
const preserveReasoningContentModels = mergeStringArray(registryEntry.preserveReasoningContentModels, provider.preserveReasoningContentModels);
|
|
288
|
+
const requiresReasoningPlaceholderModels = mergeStringArray(registryEntry.requiresReasoningPlaceholderModels, provider.requiresReasoningPlaceholderModels);
|
|
287
289
|
const reasoningSplitModels = mergeStringArray(registryEntry.reasoningSplitModels, provider.reasoningSplitModels);
|
|
288
290
|
const thinkingToggleModels = mergeStringArray(registryEntry.thinkingToggleModels, provider.thinkingToggleModels);
|
|
289
291
|
const thinkingBudgetModels = mergeStringArray(registryEntry.thinkingBudgetModels, provider.thinkingBudgetModels);
|
|
@@ -300,13 +302,17 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
300
302
|
if (userBaseUrlIsResolved) warnIfBaseUrlDiscarded(providerName, userBaseUrl, baseUrl);
|
|
301
303
|
assertProviderDestinationAllowed(providerName, { baseUrl, allowPrivateNetwork: provider.allowPrivateNetwork });
|
|
302
304
|
|
|
303
|
-
|
|
305
|
+
const resolved: OcxProviderConfig = {
|
|
304
306
|
...provider,
|
|
305
307
|
adapter: registryEntry.adapter,
|
|
306
308
|
baseUrl,
|
|
307
309
|
...(provider.responsesPath === undefined && registryEntry.responsesPath !== undefined
|
|
308
310
|
? { responsesPath: registryEntry.responsesPath }
|
|
309
311
|
: {}),
|
|
312
|
+
...(provider.requiresAdjacentResponsesToolResults === undefined
|
|
313
|
+
&& registryEntry.requiresAdjacentResponsesToolResults !== undefined
|
|
314
|
+
? { requiresAdjacentResponsesToolResults: registryEntry.requiresAdjacentResponsesToolResults }
|
|
315
|
+
: {}),
|
|
310
316
|
...(provider.supportsServiceTier === undefined && registryEntry.supportsServiceTier !== undefined
|
|
311
317
|
? { supportsServiceTier: registryEntry.supportsServiceTier }
|
|
312
318
|
: {}),
|
|
@@ -368,10 +374,13 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
368
374
|
...(noPenaltyModels ? { noPenaltyModels } : {}),
|
|
369
375
|
...(autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels } : {}),
|
|
370
376
|
...(preserveReasoningContentModels ? { preserveReasoningContentModels } : {}),
|
|
377
|
+
...(requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels } : {}),
|
|
371
378
|
...(reasoningSplitModels ? { reasoningSplitModels } : {}),
|
|
372
379
|
...(thinkingToggleModels ? { thinkingToggleModels } : {}),
|
|
373
380
|
...(thinkingBudgetModels ? { thinkingBudgetModels } : {}),
|
|
374
381
|
};
|
|
382
|
+
applyDirectReasoningEffortContracts(registryEntry, resolved, provider);
|
|
383
|
+
return resolved;
|
|
375
384
|
}
|
|
376
385
|
|
|
377
386
|
function activeProviderEntries(config: OcxConfig): [string, OcxProviderConfig][] {
|
|
@@ -675,6 +684,11 @@ export function routeModel(
|
|
|
675
684
|
return route;
|
|
676
685
|
}
|
|
677
686
|
|
|
687
|
+
/** Resolve a combo-selected provider/model target without consulting public combo aliases again. */
|
|
688
|
+
export function routeConcreteModel(config: OcxConfig, modelId: string): RouteResult {
|
|
689
|
+
return routeModelInternal(config, modelId, true, undefined);
|
|
690
|
+
}
|
|
691
|
+
|
|
678
692
|
function routeByKnownModelPattern(config: OcxConfig, modelId: string): RouteResult | undefined {
|
|
679
693
|
for (const { providerNames, prefixes } of MODEL_PROVIDER_PATTERNS) {
|
|
680
694
|
if (prefixes.some(prefix => modelId.startsWith(prefix))) {
|
package/src/routing/analytics.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* sample for the full history.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { PersistedUsageEntry
|
|
14
|
+
import type { PersistedUsageEntry } from "../usage/log";
|
|
15
15
|
import { estimateRequestCost, serviceTierContext } from "../usage/cost";
|
|
16
16
|
import { openRequestHistoryIndex, requestHistoryDb } from "./history/indexer";
|
|
17
17
|
|
|
@@ -144,14 +144,15 @@ function parseEntry(rowJson: string): PersistedUsageEntry | null {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
function attemptsOf(entry: PersistedUsageEntry | null): PersistedUsageAttempt[] | undefined {
|
|
148
|
-
return entry?.attempts;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
147
|
function cooldownTriggering(entry: PersistedUsageEntry | null, status: number): boolean {
|
|
152
148
|
if (status === 429) return true;
|
|
153
|
-
|
|
154
|
-
return attempts.some(attempt
|
|
149
|
+
if (!Array.isArray(entry?.attempts)) return false;
|
|
150
|
+
return entry.attempts.some((attempt: unknown) => {
|
|
151
|
+
if (!attempt || typeof attempt !== "object") return false;
|
|
152
|
+
const recoveryKinds = (attempt as { recoveryKinds?: unknown }).recoveryKinds;
|
|
153
|
+
return Array.isArray(recoveryKinds)
|
|
154
|
+
&& recoveryKinds.some(kind => typeof kind === "string" && COOLDOWN_RECOVERY_KINDS.has(kind));
|
|
155
|
+
});
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
function successCostUsd(
|
package/src/routing/evaluator.ts
CHANGED
|
@@ -304,14 +304,58 @@ export function evaluatePolicyProfile(
|
|
|
304
304
|
const excludedByUnknown = unknown && profile.unknownEvidence.capability === "exclude";
|
|
305
305
|
const costLimit = profile.limits.maxEstimatedCostUsd;
|
|
306
306
|
const estimatedCost = evidence.cost?.estimatedUsd;
|
|
307
|
+
const costEstimateKnown = typeof estimatedCost === "number" && Number.isFinite(estimatedCost);
|
|
307
308
|
const overCostLimit = costLimit !== undefined
|
|
308
|
-
&&
|
|
309
|
-
&&
|
|
310
|
-
&& estimatedCost > costLimit;
|
|
309
|
+
&& costEstimateKnown
|
|
310
|
+
&& estimatedCost! > costLimit;
|
|
311
311
|
if (overCostLimit) {
|
|
312
312
|
exclusions.push({ code: "cost-limit", detail: "maxEstimatedCostUsd" });
|
|
313
313
|
}
|
|
314
|
-
|
|
314
|
+
// A cap can only be *proven* satisfied when the estimate is known. The live
|
|
315
|
+
// routing path often has no usage evidence yet, so the default stays
|
|
316
|
+
// "allow" to preserve the documented dry-run contract; operators who need a
|
|
317
|
+
// genuine hard ceiling opt into "exclude". Exclusions cover only the
|
|
318
|
+
// fail-closed path; the allow path stamps `cost.capOutcome` so operators
|
|
319
|
+
// can still distinguish "known under the cap" from "unknown cost allowed".
|
|
320
|
+
const unknownCostUnderCap = costLimit !== undefined && !costEstimateKnown;
|
|
321
|
+
const unknownCostBlocked = unknownCostUnderCap
|
|
322
|
+
&& profile.limits.onUnknownCost === "exclude";
|
|
323
|
+
if (unknownCostBlocked) {
|
|
324
|
+
exclusions.push({ code: "cost-limit-unknown", detail: "maxEstimatedCostUsd" });
|
|
325
|
+
}
|
|
326
|
+
let eligible = !unsatisfied && !excludedByUnknown && !overCostLimit && !unknownCostBlocked;
|
|
327
|
+
|
|
328
|
+
// Trace/dry-run copy only: report the profile cap that was applied and the
|
|
329
|
+
// operator-visible outcome. Do not feed this copy into costScore() — that
|
|
330
|
+
// would silently change ranking when a caller supplied a different
|
|
331
|
+
// limitUsd (costScore uses limitUsd as its reference denominator).
|
|
332
|
+
let costForCandidate = evidence.cost;
|
|
333
|
+
if (costLimit !== undefined) {
|
|
334
|
+
const capOutcome = overCostLimit
|
|
335
|
+
? "exceeded" as const
|
|
336
|
+
: unknownCostBlocked
|
|
337
|
+
? "unknown-excluded" as const
|
|
338
|
+
: unknownCostUnderCap
|
|
339
|
+
? "unknown-allowed" as const
|
|
340
|
+
: "satisfied" as const;
|
|
341
|
+
if (!costEstimateKnown) {
|
|
342
|
+
// Missing or non-finite estimates are the same unknown: never stamp
|
|
343
|
+
// Infinity/NaN into the trace, and always mark incomplete.
|
|
344
|
+
const { estimatedUsd: _nonFiniteOrMissing, ...rest } = evidence.cost ?? {};
|
|
345
|
+
costForCandidate = {
|
|
346
|
+
...rest,
|
|
347
|
+
incomplete: true,
|
|
348
|
+
limitUsd: costLimit,
|
|
349
|
+
capOutcome,
|
|
350
|
+
};
|
|
351
|
+
} else {
|
|
352
|
+
costForCandidate = {
|
|
353
|
+
...evidence.cost!,
|
|
354
|
+
limitUsd: costLimit,
|
|
355
|
+
capOutcome,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
315
359
|
|
|
316
360
|
// Health scoring (RI-06): live hard cooldown is authoritative and
|
|
317
361
|
// excludes; unknown health follows the profile's unknownEvidence policy;
|
|
@@ -347,6 +391,8 @@ export function evaluatePolicyProfile(
|
|
|
347
391
|
|
|
348
392
|
// Cost scoring (RI-08): the hard per-request ceiling was already checked
|
|
349
393
|
// above; unknown cost follows the profile's unknownEvidence policy.
|
|
394
|
+
// Score against the caller's original evidence so trace stamping cannot
|
|
395
|
+
// move the costScore reference / ranking.
|
|
350
396
|
const cost = evidence.cost;
|
|
351
397
|
let costValue = cost ? costScore(cost) : null;
|
|
352
398
|
if (costValue === null && profile.unknownEvidence.cost === "exclude") {
|
|
@@ -394,7 +440,7 @@ export function evaluatePolicyProfile(
|
|
|
394
440
|
...(evidence.capability ? { capability: evidence.capability } : {}),
|
|
395
441
|
...(evidence.health ? { health: evidence.health } : {}),
|
|
396
442
|
...(evidence.quota ? { quota: evidence.quota } : {}),
|
|
397
|
-
...(
|
|
443
|
+
...(costForCandidate ? { cost: costForCandidate } : {}),
|
|
398
444
|
score,
|
|
399
445
|
};
|
|
400
446
|
candidates.push(evaluated);
|