@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/config.ts
CHANGED
|
@@ -49,6 +49,10 @@ import { assertNotRealHomeUnderTest } from "./lib/test-home-guard";
|
|
|
49
49
|
import { isLocalAttestationSecret } from "./lib/local-management-attestation";
|
|
50
50
|
import { providerDestinationConfigError } from "./lib/destination-policy";
|
|
51
51
|
import { redactSecretString } from "./lib/redact";
|
|
52
|
+
import {
|
|
53
|
+
resolveTrustedWindowsPowerShellExe,
|
|
54
|
+
resolveTrustedWindowsSystemDirectory,
|
|
55
|
+
} from "./lib/windows-elevation";
|
|
52
56
|
import { openRouterRoutingConfigError } from "./providers/openrouter-routing";
|
|
53
57
|
import {
|
|
54
58
|
isWirePinnedModel,
|
|
@@ -60,6 +64,7 @@ import {
|
|
|
60
64
|
type OcxConfig,
|
|
61
65
|
type OcxApiKeyEntry,
|
|
62
66
|
type OcxProviderConfig,
|
|
67
|
+
type ProviderCostOverlay,
|
|
63
68
|
} from "./types";
|
|
64
69
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers";
|
|
65
70
|
import {
|
|
@@ -70,6 +75,13 @@ import {
|
|
|
70
75
|
import { resolveOpenAiVirtualModel } from "./providers/openai-virtual-models";
|
|
71
76
|
import { parseDesktopProfile } from "./claude/desktop-profile";
|
|
72
77
|
import { isCodexReasoningEffort, modelRecordValue } from "./reasoning-effort";
|
|
78
|
+
import {
|
|
79
|
+
COST4_RATE_KEYS,
|
|
80
|
+
isValidCost4Rate,
|
|
81
|
+
refreshUserCostOverlays,
|
|
82
|
+
withPreservedDiskOnlyProviders,
|
|
83
|
+
} from "./usage/user-cost-overlays";
|
|
84
|
+
import { MAX_COST4_RATE } from "./usage/expected-prices";
|
|
73
85
|
import {
|
|
74
86
|
DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES,
|
|
75
87
|
MAX_APP_OWNED_MEMORY_BUDGET_MB,
|
|
@@ -613,6 +625,7 @@ const providerConfigSchema = z.object({
|
|
|
613
625
|
apiKeyTransport: z.enum(["x-api-key", "bearer"]).optional(),
|
|
614
626
|
responsesPath: z.string().min(1).optional(),
|
|
615
627
|
statelessResponses: z.boolean().optional(),
|
|
628
|
+
requiresAdjacentResponsesToolResults: z.boolean().optional(),
|
|
616
629
|
supportsServiceTier: z.boolean().optional(),
|
|
617
630
|
preserveResponsesReasoningContent: z.boolean().optional(),
|
|
618
631
|
allowPrivateNetwork: z.boolean().optional(),
|
|
@@ -699,6 +712,75 @@ export function providerHeadersConfigError(headers: unknown): string | null {
|
|
|
699
712
|
return null;
|
|
700
713
|
}
|
|
701
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Validate `providers.<name>.modelCosts`: a plain object keyed by exact model
|
|
717
|
+
* id, each value a 4-tuple of non-negative finite USD-per-1M-token rates.
|
|
718
|
+
* Returns null when valid/absent, else a human-readable error.
|
|
719
|
+
*/
|
|
720
|
+
export function providerModelCostsConfigError(value: unknown, field = "modelCosts"): string | null {
|
|
721
|
+
if (value === undefined) return null;
|
|
722
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
723
|
+
return `${field} must be a plain object keyed by model id`;
|
|
724
|
+
}
|
|
725
|
+
for (const [modelId, entry] of Object.entries(value)) {
|
|
726
|
+
if (!modelId.trim()) return `${field} keys must be nonblank model ids`;
|
|
727
|
+
// Redact secret-shaped model ids and JSON-escape control characters so a
|
|
728
|
+
// malformed write cannot echo a pasted key/secret back through the
|
|
729
|
+
// management API response.
|
|
730
|
+
const safeModelId = JSON.stringify(redactSecretString(modelId));
|
|
731
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
732
|
+
return `${field}.${safeModelId} must be an object with input, output, cacheRead, and cacheWrite (USD per 1M tokens)`;
|
|
733
|
+
}
|
|
734
|
+
const rates = entry as Record<string, unknown>;
|
|
735
|
+
for (const key of COST4_RATE_KEYS) {
|
|
736
|
+
const rate = rates[key];
|
|
737
|
+
if (!isValidCost4Rate(rate)) {
|
|
738
|
+
return `${field}.${safeModelId}.${key} must be a non-negative finite number at most ${MAX_COST4_RATE} (USD per 1M tokens)`;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Reject unknown fields: a misplaced apiKey/apiKeyPool under a cost row
|
|
742
|
+
// would otherwise be persisted and echoed verbatim by display paths that
|
|
743
|
+
// mask only top-level provider secrets.
|
|
744
|
+
const extraKeys = Object.keys(rates)
|
|
745
|
+
.filter((key) => !(COST4_RATE_KEYS as readonly string[]).includes(key));
|
|
746
|
+
if (extraKeys.length > 0) {
|
|
747
|
+
return `${field}.${safeModelId} has unexpected fields ${JSON.stringify(extraKeys.map(redactSecretString).join(", "))} — only input, output, cacheRead, and cacheWrite are allowed (USD per 1M tokens)`;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Serialize `providers.<name>.modelCosts` for display: copy ONLY the four
|
|
755
|
+
* numeric rate fields per model and DROP secret-shaped model ids, so a pasted
|
|
756
|
+
* API key in a key position cannot be echoed back by CLI/DTO display paths.
|
|
757
|
+
* The result uses a null prototype so "__proto__" remains an own row.
|
|
758
|
+
*/
|
|
759
|
+
export function sanitizeModelCostsForDisplay(costs: unknown): Record<string, ProviderCostOverlay> | undefined {
|
|
760
|
+
if (!costs || typeof costs !== "object" || Array.isArray(costs)) return undefined;
|
|
761
|
+
const out = Object.create(null) as Record<string, ProviderCostOverlay>;
|
|
762
|
+
for (const [modelId, entry] of Object.entries(costs)) {
|
|
763
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
764
|
+
const rates = entry as Record<string, unknown>;
|
|
765
|
+
const input = rates.input;
|
|
766
|
+
const output = rates.output;
|
|
767
|
+
const cacheRead = rates.cacheRead;
|
|
768
|
+
const cacheWrite = rates.cacheWrite;
|
|
769
|
+
if (
|
|
770
|
+
isValidCost4Rate(input)
|
|
771
|
+
&& isValidCost4Rate(output)
|
|
772
|
+
&& isValidCost4Rate(cacheRead)
|
|
773
|
+
&& isValidCost4Rate(cacheWrite)
|
|
774
|
+
) {
|
|
775
|
+
// Secret-shaped ids are DROPPED rather than mapped to "[REDACTED]" so
|
|
776
|
+
// distinct rows cannot collapse into one placeholder key.
|
|
777
|
+
if (redactSecretString(modelId) !== modelId) continue;
|
|
778
|
+
out[modelId] = { input, output, cacheRead, cacheWrite };
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
782
|
+
}
|
|
783
|
+
|
|
702
784
|
/** Keep the configured API-key header style scoped to Anthropic-compatible key auth. */
|
|
703
785
|
export function apiKeyTransportConfigError(
|
|
704
786
|
provider: Pick<OcxProviderConfig, "adapter" | "authMode" | "apiKeyTransport">,
|
|
@@ -1070,6 +1152,12 @@ const configSchema = z.object({
|
|
|
1070
1152
|
injectionModel: z.string().optional().catch(undefined),
|
|
1071
1153
|
injectionEffort: z.string().optional().catch(undefined),
|
|
1072
1154
|
syncCodexSubagentDefaults: z.boolean().optional().catch(undefined),
|
|
1155
|
+
// Per-primary-model fallback chains. Values must be non-empty string arrays;
|
|
1156
|
+
// malformed entries degrade to undefined rather than rejecting the whole config.
|
|
1157
|
+
subagentModelFallbackByModel: z.record(
|
|
1158
|
+
z.string(),
|
|
1159
|
+
z.array(z.string().trim().min(1)).min(1),
|
|
1160
|
+
).optional().catch(undefined),
|
|
1073
1161
|
codexShimAutoRestore: z.boolean().optional(),
|
|
1074
1162
|
pausedCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional(),
|
|
1075
1163
|
codexAccountNamespaces: codexAccountNamespacesSchema.optional(),
|
|
@@ -1162,7 +1250,7 @@ const configSchema = z.object({
|
|
|
1162
1250
|
if (!isValidProviderName(name)) {
|
|
1163
1251
|
ctx.addIssue({
|
|
1164
1252
|
code: "custom",
|
|
1165
|
-
path: ["providers", name],
|
|
1253
|
+
path: ["providers", redactSecretString(name)],
|
|
1166
1254
|
message: "provider names must use letters, numbers, dot, underscore, or hyphen and cannot be reserved JavaScript object keys or routing namespaces (policy)",
|
|
1167
1255
|
});
|
|
1168
1256
|
}
|
|
@@ -1173,7 +1261,7 @@ const configSchema = z.object({
|
|
|
1173
1261
|
code: "custom",
|
|
1174
1262
|
path: [
|
|
1175
1263
|
"providers",
|
|
1176
|
-
name,
|
|
1264
|
+
redactSecretString(name),
|
|
1177
1265
|
openRouterRoutingError.startsWith("modelOpenRouterRouting")
|
|
1178
1266
|
? "modelOpenRouterRouting"
|
|
1179
1267
|
: "openRouterRouting",
|
|
@@ -1184,7 +1272,7 @@ const configSchema = z.object({
|
|
|
1184
1272
|
if (Object.hasOwn(provider, "virtualModels")) {
|
|
1185
1273
|
ctx.addIssue({
|
|
1186
1274
|
code: "custom",
|
|
1187
|
-
path: ["providers", name, "virtualModels"],
|
|
1275
|
+
path: ["providers", redactSecretString(name), "virtualModels"],
|
|
1188
1276
|
message: "virtualModels is registry-only and must not be persisted",
|
|
1189
1277
|
});
|
|
1190
1278
|
}
|
|
@@ -1192,7 +1280,7 @@ const configSchema = z.object({
|
|
|
1192
1280
|
if (baseUrlError) {
|
|
1193
1281
|
ctx.addIssue({
|
|
1194
1282
|
code: "custom",
|
|
1195
|
-
path: ["providers", name, "baseUrl"],
|
|
1283
|
+
path: ["providers", redactSecretString(name), "baseUrl"],
|
|
1196
1284
|
message: baseUrlError,
|
|
1197
1285
|
});
|
|
1198
1286
|
} else {
|
|
@@ -1200,7 +1288,7 @@ const configSchema = z.object({
|
|
|
1200
1288
|
if (destinationError) {
|
|
1201
1289
|
ctx.addIssue({
|
|
1202
1290
|
code: "custom",
|
|
1203
|
-
path: ["providers", name, "baseUrl"],
|
|
1291
|
+
path: ["providers", redactSecretString(name), "baseUrl"],
|
|
1204
1292
|
message: destinationError,
|
|
1205
1293
|
});
|
|
1206
1294
|
}
|
|
@@ -1209,7 +1297,7 @@ const configSchema = z.object({
|
|
|
1209
1297
|
if (responsesPathError) {
|
|
1210
1298
|
ctx.addIssue({
|
|
1211
1299
|
code: "custom",
|
|
1212
|
-
path: ["providers", name, "responsesPath"],
|
|
1300
|
+
path: ["providers", redactSecretString(name), "responsesPath"],
|
|
1213
1301
|
message: responsesPathError,
|
|
1214
1302
|
});
|
|
1215
1303
|
}
|
|
@@ -1217,15 +1305,25 @@ const configSchema = z.object({
|
|
|
1217
1305
|
if (headersError) {
|
|
1218
1306
|
ctx.addIssue({
|
|
1219
1307
|
code: "custom",
|
|
1220
|
-
path: ["providers", name, "headers"],
|
|
1308
|
+
path: ["providers", redactSecretString(name), "headers"],
|
|
1221
1309
|
message: headersError,
|
|
1222
1310
|
});
|
|
1223
1311
|
}
|
|
1312
|
+
const modelCostsError = providerModelCostsConfigError((provider as { modelCosts?: unknown }).modelCosts);
|
|
1313
|
+
if (modelCostsError) {
|
|
1314
|
+
ctx.addIssue({
|
|
1315
|
+
code: "custom",
|
|
1316
|
+
// The provider key is caller-controlled and can be token-shaped; redact it
|
|
1317
|
+
// before schemaDiagnosticsError serializes the path (ocx config validate/import).
|
|
1318
|
+
path: ["providers", redactSecretString(name), "modelCosts"],
|
|
1319
|
+
message: modelCostsError,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1224
1322
|
const apiKeyTransportError = apiKeyTransportConfigError(provider as OcxProviderConfig);
|
|
1225
1323
|
if (apiKeyTransportError) {
|
|
1226
1324
|
ctx.addIssue({
|
|
1227
1325
|
code: "custom",
|
|
1228
|
-
path: ["providers", name, "apiKeyTransport"],
|
|
1326
|
+
path: ["providers", redactSecretString(name), "apiKeyTransport"],
|
|
1229
1327
|
message: apiKeyTransportError,
|
|
1230
1328
|
});
|
|
1231
1329
|
}
|
|
@@ -1238,7 +1336,7 @@ const configSchema = z.object({
|
|
|
1238
1336
|
if (modelAdaptersError) {
|
|
1239
1337
|
ctx.addIssue({
|
|
1240
1338
|
code: "custom",
|
|
1241
|
-
path: ["providers", name, "modelAdapters"],
|
|
1339
|
+
path: ["providers", redactSecretString(name), "modelAdapters"],
|
|
1242
1340
|
message: modelAdaptersError,
|
|
1243
1341
|
});
|
|
1244
1342
|
}
|
|
@@ -1251,7 +1349,7 @@ const configSchema = z.object({
|
|
|
1251
1349
|
if (preferHostedToolsError) {
|
|
1252
1350
|
ctx.addIssue({
|
|
1253
1351
|
code: "custom",
|
|
1254
|
-
path: ["providers", name, "modelPreferHostedTools"],
|
|
1352
|
+
path: ["providers", redactSecretString(name), "modelPreferHostedTools"],
|
|
1255
1353
|
message: preferHostedToolsError,
|
|
1256
1354
|
});
|
|
1257
1355
|
}
|
|
@@ -1262,7 +1360,7 @@ const configSchema = z.object({
|
|
|
1262
1360
|
if (maxInputError) {
|
|
1263
1361
|
ctx.addIssue({
|
|
1264
1362
|
code: "custom",
|
|
1265
|
-
path: ["providers", name, "modelMaxInputTokens"],
|
|
1363
|
+
path: ["providers", redactSecretString(name), "modelMaxInputTokens"],
|
|
1266
1364
|
message: maxInputError,
|
|
1267
1365
|
});
|
|
1268
1366
|
}
|
|
@@ -1273,7 +1371,7 @@ const configSchema = z.object({
|
|
|
1273
1371
|
if (reasoningSummariesError) {
|
|
1274
1372
|
ctx.addIssue({
|
|
1275
1373
|
code: "custom",
|
|
1276
|
-
path: ["providers", name, "modelSupportsReasoningSummaries"],
|
|
1374
|
+
path: ["providers", redactSecretString(name), "modelSupportsReasoningSummaries"],
|
|
1277
1375
|
message: reasoningSummariesError,
|
|
1278
1376
|
});
|
|
1279
1377
|
}
|
|
@@ -1284,7 +1382,7 @@ const configSchema = z.object({
|
|
|
1284
1382
|
if (reasoningSummaryDeliveryError) {
|
|
1285
1383
|
ctx.addIssue({
|
|
1286
1384
|
code: "custom",
|
|
1287
|
-
path: ["providers", name, "modelReasoningSummaryDelivery"],
|
|
1385
|
+
path: ["providers", redactSecretString(name), "modelReasoningSummaryDelivery"],
|
|
1288
1386
|
message: reasoningSummaryDeliveryError,
|
|
1289
1387
|
});
|
|
1290
1388
|
}
|
|
@@ -1295,7 +1393,7 @@ const configSchema = z.object({
|
|
|
1295
1393
|
if (defaultMaxOutputError) {
|
|
1296
1394
|
ctx.addIssue({
|
|
1297
1395
|
code: "custom",
|
|
1298
|
-
path: ["providers", name, "defaultMaxOutputTokens"],
|
|
1396
|
+
path: ["providers", redactSecretString(name), "defaultMaxOutputTokens"],
|
|
1299
1397
|
message: defaultMaxOutputError,
|
|
1300
1398
|
});
|
|
1301
1399
|
}
|
|
@@ -1306,7 +1404,7 @@ const configSchema = z.object({
|
|
|
1306
1404
|
if (maxOutputError) {
|
|
1307
1405
|
ctx.addIssue({
|
|
1308
1406
|
code: "custom",
|
|
1309
|
-
path: ["providers", name, "modelMaxOutputTokens"],
|
|
1407
|
+
path: ["providers", redactSecretString(name), "modelMaxOutputTokens"],
|
|
1310
1408
|
message: maxOutputError,
|
|
1311
1409
|
});
|
|
1312
1410
|
}
|
|
@@ -1321,7 +1419,7 @@ const configSchema = z.object({
|
|
|
1321
1419
|
if (!canonicalOpenAiShape) {
|
|
1322
1420
|
ctx.addIssue({
|
|
1323
1421
|
code: "custom",
|
|
1324
|
-
path: ["providers", name, "codexAccountMode"],
|
|
1422
|
+
path: ["providers", redactSecretString(name), "codexAccountMode"],
|
|
1325
1423
|
message: "codexAccountMode is valid only on the canonical built-in openai provider",
|
|
1326
1424
|
});
|
|
1327
1425
|
}
|
|
@@ -1539,6 +1637,54 @@ export function retryOn429PolicyConfigError(policy: unknown): string | null {
|
|
|
1539
1637
|
return `retryOn429.${field} is invalid (${first.message})`;
|
|
1540
1638
|
}
|
|
1541
1639
|
|
|
1640
|
+
/**
|
|
1641
|
+
* Load-time degradation for `providers.<name>.modelCosts`, mirroring
|
|
1642
|
+
* {@link sanitizeRetryOn429ForLoad}. A hand-edited malformed display-price row
|
|
1643
|
+
* must not fail the whole config parse — that would back up config.json and
|
|
1644
|
+
* fall back to defaults, dropping otherwise valid providers and the default
|
|
1645
|
+
* route for a typo in a non-runtime display field. Invalid rows are dropped
|
|
1646
|
+
* with a warning; strict rejection stays at the management/write boundary
|
|
1647
|
+
* (providerManagementConfigError).
|
|
1648
|
+
*/
|
|
1649
|
+
function sanitizeModelCostsForLoad(parsed: unknown): void {
|
|
1650
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
1651
|
+
const root = parsed as Record<string, unknown>;
|
|
1652
|
+
const providers = root.providers;
|
|
1653
|
+
if (!providers || typeof providers !== "object" || Array.isArray(providers)) return;
|
|
1654
|
+
for (const [name, provider] of Object.entries(providers as Record<string, unknown>)) {
|
|
1655
|
+
// Runs before schema validation, so the provider name is untrusted: redact
|
|
1656
|
+
// secret-shaped names and JSON-escape control characters for the warning.
|
|
1657
|
+
const safeProviderName = JSON.stringify(redactSecretString(name));
|
|
1658
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider)) continue;
|
|
1659
|
+
const p = provider as Record<string, unknown>;
|
|
1660
|
+
const costs = p.modelCosts;
|
|
1661
|
+
if (costs === undefined) continue;
|
|
1662
|
+
if (!costs || typeof costs !== "object" || Array.isArray(costs)) {
|
|
1663
|
+
delete p.modelCosts;
|
|
1664
|
+
console.warn(`⚠️ config.json providers.${safeProviderName}.modelCosts (${typeof costs}) is invalid — ignoring the overlay`);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
const costsRecord = costs as Record<string, unknown>;
|
|
1668
|
+
const hadEntries = Object.keys(costsRecord).length > 0;
|
|
1669
|
+
let kept = 0;
|
|
1670
|
+
for (const [modelId, entry] of Object.entries(costsRecord)) {
|
|
1671
|
+
// Reuse the shared per-row shape contract so the load-time sanitizer
|
|
1672
|
+
// cannot drift from the schema and the write boundary.
|
|
1673
|
+
if (providerModelCostsConfigError({ [modelId]: entry }) === null) {
|
|
1674
|
+
kept++;
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
delete costsRecord[modelId];
|
|
1678
|
+
// Redact the model id: a hand-edit can place a secret in a key name.
|
|
1679
|
+
console.warn(`⚠️ config.json providers.${safeProviderName}.modelCosts.${JSON.stringify(redactSecretString(modelId))} is invalid — ignoring the row`);
|
|
1680
|
+
}
|
|
1681
|
+
if (hadEntries && kept === 0) {
|
|
1682
|
+
delete p.modelCosts;
|
|
1683
|
+
console.warn(`⚠️ config.json providers.${safeProviderName}.modelCosts has no valid rows left — removing the overlay`);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1542
1688
|
/**
|
|
1543
1689
|
* Companion to {@link warnDegradedStreamMode} for a blank persisted `hostname`. The bind
|
|
1544
1690
|
* falls back to loopback, which is the safe direction but not what the file asked for —
|
|
@@ -1793,6 +1939,13 @@ function warnDegradedNativeSubagentConfig(rawParsed: unknown, config: OcxConfig)
|
|
|
1793
1939
|
}
|
|
1794
1940
|
}
|
|
1795
1941
|
|
|
1942
|
+
/**
|
|
1943
|
+
* Load and validate config.json into an OcxConfig. Missing files reset to
|
|
1944
|
+
* defaults and clear stale overlays. Broken existing files also fall back to
|
|
1945
|
+
* default routing (after backup), but keep the last-good cost-overlay registry
|
|
1946
|
+
* until a valid config or a genuinely missing file is observed. A partially-
|
|
1947
|
+
* invalid config is merged with defaults so providers and pool accounts survive.
|
|
1948
|
+
*/
|
|
1796
1949
|
export function loadConfig(): OcxConfig {
|
|
1797
1950
|
const dir = getConfigDir();
|
|
1798
1951
|
const configPath = getConfigPath();
|
|
@@ -1800,12 +1953,13 @@ export function loadConfig(): OcxConfig {
|
|
|
1800
1953
|
hardenExistingSecret(configPath);
|
|
1801
1954
|
hardenExistingSecret(join(dir, "auth.json"));
|
|
1802
1955
|
if (!existsSync(configPath)) {
|
|
1803
|
-
return getDefaultConfig();
|
|
1956
|
+
return withRefreshedCostOverlays(getDefaultConfig());
|
|
1804
1957
|
}
|
|
1805
1958
|
try {
|
|
1806
1959
|
const raw = readFileSync(configPath, "utf-8").replace(/^\uFEFF/, "");
|
|
1807
1960
|
const parsed = JSON.parse(raw);
|
|
1808
1961
|
sanitizeRetryOn429ForLoad(parsed);
|
|
1962
|
+
sanitizeModelCostsForLoad(parsed);
|
|
1809
1963
|
const result = configSchema.safeParse(parsed);
|
|
1810
1964
|
if (result.success) {
|
|
1811
1965
|
const config = normalizeApiKeyIds(result.data as OcxConfig);
|
|
@@ -1817,7 +1971,7 @@ export function loadConfig(): OcxConfig {
|
|
|
1817
1971
|
warnDegradedNativeSubagentConfig(parsed, config);
|
|
1818
1972
|
warnDegradedCodexAccountPicker(parsed);
|
|
1819
1973
|
warnDegradedUpstreamHostCircuitThreshold(parsed);
|
|
1820
|
-
return normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed);
|
|
1974
|
+
return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
|
|
1821
1975
|
}
|
|
1822
1976
|
// Schema validation failed — merge defaults into the raw object instead of
|
|
1823
1977
|
// discarding it entirely, so pool accounts and providers survive a missing
|
|
@@ -1839,7 +1993,7 @@ export function loadConfig(): OcxConfig {
|
|
|
1839
1993
|
warnDegradedNativeSubagentConfig(parsed, config);
|
|
1840
1994
|
warnDegradedCodexAccountPicker(parsed);
|
|
1841
1995
|
warnDegradedUpstreamHostCircuitThreshold(parsed);
|
|
1842
|
-
return normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed);
|
|
1996
|
+
return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
|
|
1843
1997
|
}
|
|
1844
1998
|
// Merge couldn't fix it — truly broken config
|
|
1845
1999
|
warnAndBackupInvalidConfig(configPath, result.error);
|
|
@@ -1850,6 +2004,12 @@ export function loadConfig(): OcxConfig {
|
|
|
1850
2004
|
}
|
|
1851
2005
|
}
|
|
1852
2006
|
|
|
2007
|
+
/** Refresh the user cost-overlay registry from `config` and return it unchanged. */
|
|
2008
|
+
function withRefreshedCostOverlays(config: OcxConfig): OcxConfig {
|
|
2009
|
+
refreshUserCostOverlays(config);
|
|
2010
|
+
return config;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1853
2013
|
export type ConfigDiagnostics = {
|
|
1854
2014
|
config: OcxConfig;
|
|
1855
2015
|
source: "default" | "file" | "fallback";
|
|
@@ -2085,6 +2245,7 @@ function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics {
|
|
|
2085
2245
|
// schema and send the caller a default-config fallback (the config command could then
|
|
2086
2246
|
// persist that fallback over the user's providers/keys).
|
|
2087
2247
|
sanitizeRetryOn429ForLoad(parsed);
|
|
2248
|
+
sanitizeModelCostsForLoad(parsed);
|
|
2088
2249
|
const result = configSchema.safeParse(parsed);
|
|
2089
2250
|
if (result.success) {
|
|
2090
2251
|
return validFileConfigDiagnostics(normalizeApiKeyIds(result.data as OcxConfig), parsed);
|
|
@@ -2368,18 +2529,41 @@ export const withExpectedConfigGenerationSync: WithExpectedConfigGenerationSync
|
|
|
2368
2529
|
}
|
|
2369
2530
|
};
|
|
2370
2531
|
|
|
2532
|
+
/**
|
|
2533
|
+
* Atomic config.json write WITHOUT the mutation lock; callers must hold
|
|
2534
|
+
* `withConfigMutationLockSync`. Returns true when bytes changed. Refreshes the
|
|
2535
|
+
* cost-overlay registry from the persisted config so runtime estimates follow
|
|
2536
|
+
* every save path.
|
|
2537
|
+
*/
|
|
2371
2538
|
function persistConfigUnlocked(config: OcxConfig): boolean {
|
|
2372
2539
|
const configPath = getConfigPath();
|
|
2373
|
-
|
|
2540
|
+
// External editors can add provider rows the live config deliberately does
|
|
2541
|
+
// not route with yet; merge them at the serialization boundary so an
|
|
2542
|
+
// unrelated in-process save cannot erase the provider or its overlay.
|
|
2543
|
+
const persisted = withPreservedDiskOnlyProviders(config);
|
|
2544
|
+
const bytes = JSON.stringify(persisted, null, 2) + "\n";
|
|
2545
|
+
let unchanged = false;
|
|
2374
2546
|
try {
|
|
2375
|
-
|
|
2547
|
+
unchanged = readFileSync(configPath, "utf8") === bytes;
|
|
2376
2548
|
} catch (error) {
|
|
2377
2549
|
if (!isMissingPathError(error)) throw error;
|
|
2378
2550
|
}
|
|
2551
|
+
// Keep the runtime overlay registry in sync with EVERY persist path,
|
|
2552
|
+
// including byte-identical saves: a cooperating CLI process may have written
|
|
2553
|
+
// the same bytes (e.g. before a proxy notification), and Logs/Usage must
|
|
2554
|
+
// adopt the overlay without waiting for a changed save or restart.
|
|
2555
|
+
if (unchanged) {
|
|
2556
|
+
refreshUserCostOverlays(persisted);
|
|
2557
|
+
return false;
|
|
2558
|
+
}
|
|
2379
2559
|
atomicWriteFile(configPath, bytes);
|
|
2560
|
+
// For changed saves, refresh only AFTER the write succeeded so a failed
|
|
2561
|
+
// write cannot leave estimates reflecting configuration never persisted.
|
|
2562
|
+
refreshUserCostOverlays(persisted);
|
|
2380
2563
|
return true;
|
|
2381
2564
|
}
|
|
2382
2565
|
|
|
2566
|
+
/** Persist `config` to config.json under the config-mutation lock. */
|
|
2383
2567
|
export function saveConfig(config: OcxConfig): void {
|
|
2384
2568
|
// Keep the real-home assertion ahead of even lock-directory preparation.
|
|
2385
2569
|
assertNotRealHomeUnderTest(getConfigDir());
|
|
@@ -2654,6 +2838,10 @@ export function reconcileLiveConfigFromDisk(config: OcxConfig, persistedBaseline
|
|
|
2654
2838
|
else config.claudeCode = structuredClone(persisted.claudeCode);
|
|
2655
2839
|
claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
|
|
2656
2840
|
}
|
|
2841
|
+
// The reconciliation may have adopted a providers.<name>.modelCosts edit made
|
|
2842
|
+
// by a cooperating process while the OAuth login was pending; keep the overlay
|
|
2843
|
+
// registry (and the usage-cache overlay version) in sync with the live config.
|
|
2844
|
+
refreshUserCostOverlays(config);
|
|
2657
2845
|
}
|
|
2658
2846
|
|
|
2659
2847
|
/** The literal file, with no schema merge or default injection. */
|
|
@@ -3063,14 +3251,51 @@ export function verifyPidIdentity(candidatePid: number): number | null {
|
|
|
3063
3251
|
return isLikelyOcxStartProcess(candidatePid) ? candidatePid : null;
|
|
3064
3252
|
}
|
|
3065
3253
|
|
|
3254
|
+
type ProcessCommandLineExec = (
|
|
3255
|
+
executable: string,
|
|
3256
|
+
args: string[],
|
|
3257
|
+
options: {
|
|
3258
|
+
encoding: BufferEncoding;
|
|
3259
|
+
stdio: ["ignore", "pipe", "ignore"];
|
|
3260
|
+
timeout: number;
|
|
3261
|
+
windowsHide: boolean;
|
|
3262
|
+
},
|
|
3263
|
+
) => string;
|
|
3264
|
+
|
|
3265
|
+
const defaultProcessCommandLineExec: ProcessCommandLineExec = (executable, args, options) =>
|
|
3266
|
+
execFileSync(executable, args, options);
|
|
3267
|
+
let processCommandLineExec = defaultProcessCommandLineExec;
|
|
3268
|
+
let processCommandLinePlatformForTests: NodeJS.Platform | null = null;
|
|
3269
|
+
|
|
3270
|
+
/** Test-only seam for verifying the exact system executable selected by pid identity probes. */
|
|
3271
|
+
export function setProcessCommandLineExecForTests(next: ProcessCommandLineExec | null): void {
|
|
3272
|
+
processCommandLineExec = next ?? defaultProcessCommandLineExec;
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
/** Test-only seam so cross-platform tests do not mutate process.platform. */
|
|
3276
|
+
export function setProcessCommandLinePlatformForTests(next: NodeJS.Platform | null): void {
|
|
3277
|
+
processCommandLinePlatformForTests = next;
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3066
3280
|
function readProcessCommandLine(pid: number): string | undefined {
|
|
3281
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return undefined;
|
|
3282
|
+
const platform = processCommandLinePlatformForTests ?? process.platform;
|
|
3067
3283
|
try {
|
|
3068
|
-
if (
|
|
3284
|
+
if (platform === "linux") {
|
|
3285
|
+
try {
|
|
3286
|
+
const output = readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
3287
|
+
const value = output.replace(/\0/g, " ").trim();
|
|
3288
|
+
if (value) return value;
|
|
3289
|
+
} catch {
|
|
3290
|
+
/* procfs unavailable — use the fixed ps fallback below */
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
if (platform === "win32") {
|
|
3069
3294
|
// Prefer WMIC over PowerShell: much faster cold start, and windowsHide avoids console flash.
|
|
3070
3295
|
// Fall back to PowerShell when WMIC is absent (newer Windows images).
|
|
3071
|
-
const wmic =
|
|
3296
|
+
const wmic = join(resolveTrustedWindowsSystemDirectory(), "wbem", "WMIC.exe");
|
|
3072
3297
|
try {
|
|
3073
|
-
const output =
|
|
3298
|
+
const output = processCommandLineExec(wmic, [
|
|
3074
3299
|
"process", "where", `ProcessId=${pid}`, "get", "CommandLine", "/VALUE",
|
|
3075
3300
|
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000, windowsHide: true });
|
|
3076
3301
|
const match = /^CommandLine=(.*)$/m.exec(output.replace(/\r/g, ""));
|
|
@@ -3079,7 +3304,7 @@ function readProcessCommandLine(pid: number): string | undefined {
|
|
|
3079
3304
|
} catch {
|
|
3080
3305
|
/* WMIC missing or failed — fall through */
|
|
3081
3306
|
}
|
|
3082
|
-
const output =
|
|
3307
|
+
const output = processCommandLineExec(resolveTrustedWindowsPowerShellExe(), [
|
|
3083
3308
|
"-NoProfile",
|
|
3084
3309
|
"-NoLogo",
|
|
3085
3310
|
"-NonInteractive",
|
|
@@ -3090,13 +3315,21 @@ function readProcessCommandLine(pid: number): string | undefined {
|
|
|
3090
3315
|
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000, windowsHide: true });
|
|
3091
3316
|
return output.trim() || undefined;
|
|
3092
3317
|
}
|
|
3093
|
-
const
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3318
|
+
for (const ps of ["/bin/ps", "/usr/bin/ps"]) {
|
|
3319
|
+
try {
|
|
3320
|
+
const output = processCommandLineExec(ps, ["-p", String(pid), "-o", "command="], {
|
|
3321
|
+
encoding: "utf-8",
|
|
3322
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3323
|
+
timeout: 1000,
|
|
3324
|
+
windowsHide: true,
|
|
3325
|
+
});
|
|
3326
|
+
const value = output.trim();
|
|
3327
|
+
if (value) return value;
|
|
3328
|
+
} catch {
|
|
3329
|
+
/* try the other fixed system path */
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
return undefined;
|
|
3100
3333
|
} catch {
|
|
3101
3334
|
return undefined;
|
|
3102
3335
|
}
|
package/src/images/loop.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { existsSync } from "node:fs";
|
|
|
15
15
|
import { pathToFileURL } from "node:url";
|
|
16
16
|
import { createAdapterEventQueue } from "../adapters/run-turn-queue";
|
|
17
17
|
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderContinuationState, OcxRequestOptions, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
|
|
18
|
-
import { namespacedToolName } from "../types";
|
|
18
|
+
import { namespacedToolName, toolChoiceToolPredicate } from "../types";
|
|
19
19
|
import type { AttemptRecoveryKind } from "../usage/log";
|
|
20
20
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
21
21
|
import { clearableDeadline, idleDeadline } from "../lib/abort";
|
|
@@ -662,7 +662,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
662
662
|
const toolNsMap = new Map<string, { namespace: string; name: string }>();
|
|
663
663
|
const freeform = new Set<string>();
|
|
664
664
|
const toolSearch = new Set<string>();
|
|
665
|
+
const toolAllowed = toolChoiceToolPredicate(parsed.options.toolChoice);
|
|
665
666
|
for (const t of parsed.context.tools ?? []) {
|
|
667
|
+
if (!toolAllowed(t)) continue;
|
|
666
668
|
if (t.namespace) toolNsMap.set(namespacedToolName(t.namespace, t.name), { namespace: t.namespace, name: t.name });
|
|
667
669
|
if (t.freeform) freeform.add(t.name);
|
|
668
670
|
if (t.toolSearch) toolSearch.add(t.name);
|
package/src/images/plan.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
2
|
+
import { toolChoiceToolPredicate } from "../types";
|
|
2
3
|
import type { ImageBridgePlan, VideoBridgePlan } from "./types";
|
|
3
4
|
import { resolveEnvValue } from "../config";
|
|
4
5
|
import { getProviderRegistryEntry } from "../providers/registry";
|
|
@@ -46,6 +47,12 @@ export async function planImageBridge(
|
|
|
46
47
|
): Promise<ImageBridgePlan | undefined> {
|
|
47
48
|
if (config.images?.bridgeEnabled !== true) return undefined;
|
|
48
49
|
if (!parsed._imageGeneration) return undefined;
|
|
50
|
+
const toolAllowed = toolChoiceToolPredicate(parsed.options.toolChoice);
|
|
51
|
+
const toolNames = new Set(
|
|
52
|
+
[...parsed._imageGeneration.toolNames, IMAGE_GEN_TOOL_NAME]
|
|
53
|
+
.filter(name => toolAllowed({ name })),
|
|
54
|
+
);
|
|
55
|
+
if (toolNames.size === 0) return undefined;
|
|
49
56
|
// Don't intercept for OpenAI native passthrough
|
|
50
57
|
const host = (() => { try { return new URL(routedProvider.baseUrl).hostname; } catch { return ""; } })();
|
|
51
58
|
if (host === "api.openai.com") return undefined;
|
|
@@ -57,9 +64,7 @@ export async function planImageBridge(
|
|
|
57
64
|
const registryEntry = getProviderRegistryEntry("xai");
|
|
58
65
|
const pinnedBaseUrl = (registryEntry?.baseUrl ?? "https://api.x.ai/v1").replace(/\/+$/, "");
|
|
59
66
|
// The synthetic tool injected into the conversation is named IMAGE_GEN_TOOL_NAME,
|
|
60
|
-
// which is what the model will actually call.
|
|
61
|
-
const toolNames = new Set(parsed._imageGeneration.toolNames);
|
|
62
|
-
toolNames.add(IMAGE_GEN_TOOL_NAME);
|
|
67
|
+
// which is what the model will actually call. toolNames also retains authorized hosted aliases.
|
|
63
68
|
const original = parsed._imageGeneration.originalTool;
|
|
64
69
|
const hostedSize = typeof original?.size === "string" ? original.size : undefined;
|
|
65
70
|
const hostedQuality = typeof original?.quality === "string" ? original.quality : undefined;
|
|
@@ -95,16 +100,6 @@ export async function planVideoBridge(
|
|
|
95
100
|
routedProvider: OcxProviderConfig,
|
|
96
101
|
): Promise<VideoBridgePlan | undefined> {
|
|
97
102
|
if (config.images?.videoBridgeEnabled !== true) return undefined;
|
|
98
|
-
// Don't intercept for OpenAI native passthrough
|
|
99
|
-
const host = (() => { try { return new URL(routedProvider.baseUrl).hostname; } catch { return ""; } })();
|
|
100
|
-
if (host === "api.openai.com") return undefined;
|
|
101
|
-
const found = findXaiProvider(config);
|
|
102
|
-
if (!found) return undefined;
|
|
103
|
-
const token = resolveXaiImageApiKey(found.provider);
|
|
104
|
-
if (!token) return undefined;
|
|
105
|
-
// Pin the baseUrl to the registry entry, ignoring any config-level baseUrl override.
|
|
106
|
-
const registryEntry = getProviderRegistryEntry("xai");
|
|
107
|
-
const pinnedBaseUrl = (registryEntry?.baseUrl ?? "https://api.x.ai/v1").replace(/\/+$/, "");
|
|
108
103
|
const toolNames = new Set<string>();
|
|
109
104
|
toolNames.add(VIDEO_GEN_TOOL_NAME);
|
|
110
105
|
// Collect any existing function tools whose name matches a video_gen alias
|
|
@@ -118,6 +113,21 @@ export async function planVideoBridge(
|
|
|
118
113
|
toolNames.add(fnName);
|
|
119
114
|
}
|
|
120
115
|
}
|
|
116
|
+
const toolAllowed = toolChoiceToolPredicate(parsed.options?.toolChoice);
|
|
117
|
+
for (const name of toolNames) {
|
|
118
|
+
if (!toolAllowed({ name })) toolNames.delete(name);
|
|
119
|
+
}
|
|
120
|
+
if (toolNames.size === 0) return undefined;
|
|
121
|
+
// Don't intercept for OpenAI native passthrough
|
|
122
|
+
const host = (() => { try { return new URL(routedProvider.baseUrl).hostname; } catch { return ""; } })();
|
|
123
|
+
if (host === "api.openai.com") return undefined;
|
|
124
|
+
const found = findXaiProvider(config);
|
|
125
|
+
if (!found) return undefined;
|
|
126
|
+
const token = resolveXaiImageApiKey(found.provider);
|
|
127
|
+
if (!token) return undefined;
|
|
128
|
+
// Pin the baseUrl to the registry entry, ignoring any config-level baseUrl override.
|
|
129
|
+
const registryEntry = getProviderRegistryEntry("xai");
|
|
130
|
+
const pinnedBaseUrl = (registryEntry?.baseUrl ?? "https://api.x.ai/v1").replace(/\/+$/, "");
|
|
121
131
|
const timeoutMs = clampImageTimeoutMs(config.images?.videoTimeoutMs);
|
|
122
132
|
const keepRaw = config.images?.artifactsKeepCount;
|
|
123
133
|
const artifactsKeepCount =
|