@bitkyc08/opencodex 2.7.39 → 2.7.40
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 +4 -4
- package/gui/dist/assets/index-CMip1DzF.css +1 -0
- package/gui/dist/assets/index-cydcmbzC.js +52 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/cursor/arg-normalize.ts +23 -7
- package/src/adapters/cursor/live-transport.ts +26 -14
- package/src/adapters/cursor/native-exec-fs.ts +1 -1
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +1 -1
- package/src/adapters/cursor/protobuf-events.ts +72 -13
- package/src/adapters/cursor/protobuf-request.ts +82 -11
- package/src/adapters/cursor/request-builder.ts +35 -11
- package/src/adapters/cursor/tool-definitions.ts +175 -30
- package/src/adapters/openai-chat.ts +28 -7
- package/src/adapters/openai-responses.ts +150 -4
- package/src/bridge.ts +20 -1
- package/src/claude/outbound.ts +91 -6
- package/src/codex/auth-api.ts +12 -25
- package/src/codex/auth-context.ts +48 -3
- package/src/codex/catalog/provider-fetch.ts +56 -24
- package/src/codex/model-cache.ts +23 -0
- package/src/codex/quota.ts +120 -0
- package/src/codex/routing.ts +178 -9
- package/src/config.ts +56 -1
- package/src/providers/openai-sidecar.ts +8 -1
- package/src/providers/openai-tiers.ts +18 -0
- package/src/server/adapter-resolve.ts +24 -10
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +4 -0
- package/src/server/claude-messages.ts +4 -0
- package/src/server/index.ts +3 -1
- package/src/server/live.ts +56 -0
- package/src/server/memory-watchdog.ts +1 -1
- package/src/server/responses/compact.ts +40 -10
- package/src/server/responses/core.ts +180 -26
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/service.ts +113 -30
- package/src/types.ts +52 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/anthropic-executor.ts +3 -1
- package/src/web-search/index.ts +7 -1
- package/src/web-search/loop.ts +17 -3
- package/README.ja.md +0 -445
- package/README.ko.md +0 -435
- package/README.ru.md +0 -486
- package/README.zh-CN.md +0 -411
- package/gui/dist/assets/index-B-cheu55.js +0 -52
- package/gui/dist/assets/index-oOZcqVmj.css +0 -1
package/src/service.ts
CHANGED
|
@@ -472,26 +472,103 @@ function taskXmlSection(xml: string, tag: string): string {
|
|
|
472
472
|
return new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${tag}>`, "i").exec(xml)?.[1] ?? "";
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
/** Drop comments and CDATA so a commented-out decoy cannot satisfy any check. */
|
|
476
|
+
function taskXmlWithoutCommentsAndCdata(xml: string): string {
|
|
477
|
+
return xml.replace(/<!--[\s\S]*?-->/g, "").replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Count occurrences of an unprefixed tag, including the self-closing form. The
|
|
482
|
+
* element boundary matters: `<EnabledExtra>` must not count as `Enabled`.
|
|
483
|
+
*/
|
|
484
|
+
function taskXmlElementCount(xml: string, tag: string): number {
|
|
485
|
+
return xml.match(new RegExp(`<${tag}(?:\\s[^>]*?)?\\s*\\/?>`, "gi"))?.length ?? 0;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* True when a namespace-prefixed form of the tag appears. A prefixed element bound
|
|
490
|
+
* to the task namespace carries a real value, but this module parses by regex and
|
|
491
|
+
* cannot resolve prefixes — so it fails closed instead of reading the element as
|
|
492
|
+
* absent (which would silently apply the schema default).
|
|
493
|
+
*/
|
|
494
|
+
function taskXmlHasPrefixedTag(xml: string, tag: string): boolean {
|
|
495
|
+
return new RegExp(`<[A-Za-z_][\\w.-]*:${tag}(?:[\\s/>])`, "i").test(xml);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Compare an element that Task Scheduler may omit when exporting a registered task.
|
|
500
|
+
* Absence means the documented schema default (#432); a present element must still
|
|
501
|
+
* match exactly, so a malformed or explicitly unsafe value never reads as healthy.
|
|
502
|
+
*/
|
|
503
|
+
function taskXmlOptionalValueEquals(xml: string, tag: string, expected: string): boolean {
|
|
504
|
+
// Check the prefixed form first: treating `<t:Enabled>false</t:Enabled>` as an
|
|
505
|
+
// omission would turn an explicitly disabled task into a healthy one.
|
|
506
|
+
if (taskXmlHasPrefixedTag(xml, tag)) return false;
|
|
507
|
+
const count = taskXmlElementCount(xml, tag);
|
|
508
|
+
if (count === 0) return true;
|
|
509
|
+
if (count > 1) return false;
|
|
510
|
+
const value = new RegExp(`<${tag}(?:\\s[^>]*?)?>\\s*([^<]*?)\\s*<\\/${tag}>`, "i").exec(xml)?.[1];
|
|
511
|
+
return value?.trim().toLowerCase() === expected.toLowerCase();
|
|
512
|
+
}
|
|
513
|
+
|
|
475
514
|
/** Validate the security/lifecycle-critical fields of the registered scheduler task. */
|
|
476
515
|
export function windowsTaskRegistrationHealthy(
|
|
477
516
|
xml: string,
|
|
478
517
|
wscript = windowsWscript(),
|
|
479
518
|
launcher = windowsLauncherVbsPath(),
|
|
480
519
|
): boolean {
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
520
|
+
const scrubbed = taskXmlWithoutCommentsAndCdata(xml);
|
|
521
|
+
// taskXmlSection() takes the FIRST match and the schema allows arbitrary XML under
|
|
522
|
+
// Task/Data, so a Data block placed before the real sections could shadow them.
|
|
523
|
+
// We never emit Data, so its presence alone disqualifies the registration. Both
|
|
524
|
+
// forms are rejected because taskXmlElementCount() ignores prefixed tags.
|
|
525
|
+
if (taskXmlElementCount(scrubbed, "Data") > 0 || taskXmlHasPrefixedTag(scrubbed, "Data")) return false;
|
|
526
|
+
const triggers = taskXmlSection(scrubbed, "Triggers");
|
|
527
|
+
const trigger = taskXmlSection(triggers, "LogonTrigger");
|
|
528
|
+
const principal = taskXmlSection(scrubbed, "Principal");
|
|
529
|
+
const settings = taskXmlSection(scrubbed, "Settings");
|
|
530
|
+
const action = taskXmlSection(scrubbed, "Exec");
|
|
531
|
+
// A self-closing <LogonTrigger /> leaves an empty section, so look for the element
|
|
532
|
+
// itself — scoped to <Triggers> so a decoy elsewhere cannot satisfy it.
|
|
533
|
+
return taskXmlElementCount(triggers, "LogonTrigger") > 0
|
|
534
|
+
&& taskXmlOptionalValueEquals(trigger, "Enabled", "true")
|
|
486
535
|
&& /<LogonType>\s*InteractiveToken\s*<\/LogonType>/i.test(principal)
|
|
487
|
-
&&
|
|
488
|
-
&&
|
|
536
|
+
&& taskXmlOptionalValueEquals(principal, "RunLevel", "LeastPrivilege")
|
|
537
|
+
&& taskXmlOptionalValueEquals(settings, "Enabled", "true")
|
|
489
538
|
&& /<MultipleInstancesPolicy>\s*IgnoreNew\s*<\/MultipleInstancesPolicy>/i.test(settings)
|
|
490
539
|
&& /<ExecutionTimeLimit>\s*PT0S\s*<\/ExecutionTimeLimit>/i.test(settings)
|
|
491
540
|
&& action.includes(`<Command>${taskXmlString(wscript)}</Command>`)
|
|
492
541
|
&& action.includes(`<Arguments>${taskXmlString(`/b /nologo "${launcher}"`)}</Arguments>`);
|
|
493
542
|
}
|
|
494
543
|
|
|
544
|
+
export interface WindowsSchedulerXmlState {
|
|
545
|
+
installed: boolean;
|
|
546
|
+
enabled: boolean;
|
|
547
|
+
registrationHealthy: boolean;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Single source of truth for reading a registered task's XML. Both the status
|
|
552
|
+
* diagnostic and its tests go through here, so a partial fix cannot leave one
|
|
553
|
+
* caller on an older, stricter reading of the same document (#432).
|
|
554
|
+
*/
|
|
555
|
+
export function readWindowsSchedulerXmlState(
|
|
556
|
+
xml: string,
|
|
557
|
+
wscript?: string,
|
|
558
|
+
launcher?: string,
|
|
559
|
+
): WindowsSchedulerXmlState {
|
|
560
|
+
const installed = xml.length > 0;
|
|
561
|
+
if (!installed) return { installed: false, enabled: false, registrationHealthy: false };
|
|
562
|
+
const scrubbed = taskXmlWithoutCommentsAndCdata(xml);
|
|
563
|
+
const hasData = taskXmlElementCount(scrubbed, "Data") > 0 || taskXmlHasPrefixedTag(scrubbed, "Data");
|
|
564
|
+
const settings = hasData ? "" : taskXmlSection(scrubbed, "Settings");
|
|
565
|
+
return {
|
|
566
|
+
installed: true,
|
|
567
|
+
enabled: !hasData && taskXmlOptionalValueEquals(settings, "Enabled", "true"),
|
|
568
|
+
registrationHealthy: windowsTaskRegistrationHealthy(xml, wscript, launcher),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
495
572
|
// ── macOS (launchd) ──
|
|
496
573
|
function installLaunchd(): void {
|
|
497
574
|
const dir = join(homedir(), "Library", "LaunchAgents");
|
|
@@ -878,9 +955,15 @@ export function serviceStartableFromTray(service: ServiceDiagnostic): boolean {
|
|
|
878
955
|
}
|
|
879
956
|
|
|
880
957
|
export interface WindowsServiceDiagnosticInputs {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
958
|
+
/**
|
|
959
|
+
* Raw `schtasks /query /xml` output; empty when no task is registered. Passed as
|
|
960
|
+
* XML rather than pre-computed booleans so every caller reads the document through
|
|
961
|
+
* readWindowsSchedulerXmlState() — a second, stricter reading elsewhere would
|
|
962
|
+
* silently reintroduce the stale-status false positive (#432).
|
|
963
|
+
*/
|
|
964
|
+
schedulerXml: string;
|
|
965
|
+
/** Whether the on-disk service assets exist. A filesystem concern, not an XML one. */
|
|
966
|
+
schedulerAssetsPresent: boolean;
|
|
884
967
|
nativeStatus: "started" | "stopped" | "nonexistent" | "unknown";
|
|
885
968
|
recordedBackend: ServiceBackend | null;
|
|
886
969
|
staleBakedPaths: boolean;
|
|
@@ -889,37 +972,41 @@ export interface WindowsServiceDiagnosticInputs {
|
|
|
889
972
|
}
|
|
890
973
|
|
|
891
974
|
export function deriveWindowsServiceDiagnostic(inputs: WindowsServiceDiagnosticInputs): ServiceDiagnostic {
|
|
975
|
+
const schedulerState = readWindowsSchedulerXmlState(inputs.schedulerXml);
|
|
976
|
+
const schedulerInstalled = schedulerState.installed;
|
|
977
|
+
const schedulerEnabled = schedulerState.enabled;
|
|
978
|
+
const schedulerAssetsHealthy = inputs.schedulerAssetsPresent && schedulerState.registrationHealthy;
|
|
892
979
|
const nativeInstalled = inputs.nativeStatus !== "nonexistent";
|
|
893
|
-
const conflict =
|
|
894
|
-
const backendStateMismatch =
|
|
980
|
+
const conflict = schedulerInstalled && nativeInstalled;
|
|
981
|
+
const backendStateMismatch = schedulerInstalled
|
|
895
982
|
? inputs.recordedBackend !== "scheduler"
|
|
896
983
|
: nativeInstalled && inputs.recordedBackend !== "native";
|
|
897
984
|
const stale = inputs.staleBakedPaths
|
|
898
|
-
|| (
|
|
985
|
+
|| (schedulerInstalled && !schedulerAssetsHealthy)
|
|
899
986
|
|| backendStateMismatch
|
|
900
987
|
|| (inputs.nativeStatus === "nonexistent" && inputs.nativeRepairAssetsOnly);
|
|
901
|
-
const backend =
|
|
902
|
-
const enabled =
|
|
903
|
-
const running = nativeInstalled ? inputs.nativeStatus === "started" :
|
|
988
|
+
const backend = schedulerInstalled ? "scheduler" : nativeInstalled ? "native" : null;
|
|
989
|
+
const enabled = schedulerInstalled ? schedulerEnabled : inputs.nativeStatus === "started";
|
|
990
|
+
const running = nativeInstalled ? inputs.nativeStatus === "started" : schedulerInstalled && schedulerEnabled;
|
|
904
991
|
const viable = !conflict && !stale
|
|
905
|
-
&& (
|
|
992
|
+
&& (schedulerInstalled ? schedulerEnabled && schedulerAssetsHealthy : inputs.nativeStatus === "started");
|
|
906
993
|
const startable = !conflict && !stale
|
|
907
|
-
&& (
|
|
908
|
-
?
|
|
994
|
+
&& (schedulerInstalled
|
|
995
|
+
? schedulerEnabled && schedulerAssetsHealthy
|
|
909
996
|
: inputs.nativeStatus === "started" || inputs.nativeStatus === "stopped");
|
|
910
997
|
const detail = conflict
|
|
911
998
|
? "CONFLICT: Task Scheduler and native WinSW are both present — run 'ocx service uninstall' then reinstall one"
|
|
912
999
|
: stale
|
|
913
1000
|
? "stale or missing service assets — run 'ocx service install' to repair"
|
|
914
|
-
:
|
|
915
|
-
?
|
|
1001
|
+
: schedulerInstalled
|
|
1002
|
+
? schedulerEnabled ? "Task Scheduler enabled" : "Task Scheduler disabled"
|
|
916
1003
|
: nativeInstalled
|
|
917
1004
|
? `native (WinSW ${WINSW_VERSION}): ${inputs.nativeStatus}`
|
|
918
1005
|
: "not installed";
|
|
919
1006
|
const summary = backend ? `installed, ${detail} (${inputs.diagnostics})` : `not installed (${inputs.diagnostics})`;
|
|
920
1007
|
return {
|
|
921
1008
|
supported: true,
|
|
922
|
-
installed:
|
|
1009
|
+
installed: schedulerInstalled || nativeInstalled,
|
|
923
1010
|
enabled,
|
|
924
1011
|
running,
|
|
925
1012
|
viable,
|
|
@@ -951,20 +1038,16 @@ export function diagnoseService(): ServiceDiagnostic {
|
|
|
951
1038
|
}
|
|
952
1039
|
if (process.platform === "win32") {
|
|
953
1040
|
const schedulerXml = statusWindowsXml();
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
const schedulerEnabled = schedulerInstalled && /<Enabled>\s*true\s*<\/Enabled>/i.test(schedulerSettings);
|
|
957
|
-
const schedulerAssets = [windowsServiceScriptPath(), windowsLauncherVbsPath(), windowsTaskXmlPath()].every(existsSync)
|
|
958
|
-
&& windowsTaskRegistrationHealthy(schedulerXml);
|
|
1041
|
+
const schedulerAssetsPresent = [windowsServiceScriptPath(), windowsLauncherVbsPath(), windowsTaskXmlPath()]
|
|
1042
|
+
.every(existsSync);
|
|
959
1043
|
const nativeStatus = statusWinswRaw();
|
|
960
1044
|
const installState = readServiceInstallState();
|
|
961
1045
|
const recordedBackend: ServiceBackend | null = !installState
|
|
962
1046
|
? null
|
|
963
1047
|
: installState.backend === "native" ? "native" : "scheduler";
|
|
964
1048
|
return deriveWindowsServiceDiagnostic({
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
schedulerAssetsHealthy: schedulerAssets,
|
|
1049
|
+
schedulerXml,
|
|
1050
|
+
schedulerAssetsPresent,
|
|
968
1051
|
nativeStatus,
|
|
969
1052
|
recordedBackend,
|
|
970
1053
|
staleBakedPaths: bakedServicePathsDiagnostic() !== null,
|
package/src/types.ts
CHANGED
|
@@ -246,6 +246,8 @@ export type AdapterEvent =
|
|
|
246
246
|
| { type: "tool_call_start"; id: string; name: string }
|
|
247
247
|
| { type: "tool_call_delta"; arguments: string }
|
|
248
248
|
| { type: "tool_call_end" }
|
|
249
|
+
/** Internal boundary between a guarded first pass and its one-shot continuation. */
|
|
250
|
+
| { type: "assistant_boundary" }
|
|
249
251
|
// Native web-search activity surfaced by the web-search sidecar so Codex renders a "Searched the
|
|
250
252
|
// web" cell. Emitted as a lifecycle PAIR at real wall-clock moments by src/web-search/loop.ts
|
|
251
253
|
// (routed adapters never emit these): `begin` right before the sidecar runs so Codex shows the
|
|
@@ -718,6 +720,17 @@ export interface ResponsesItemIdRepairConfig {
|
|
|
718
720
|
|
|
719
721
|
export interface OcxProviderConfig {
|
|
720
722
|
adapter: string;
|
|
723
|
+
/**
|
|
724
|
+
* Per-model wire override, keyed by the upstream native model id (after namespace
|
|
725
|
+
* and combo resolution). A single gateway can front models that speak different
|
|
726
|
+
* wires — Grok needs the Responses API for hosted web_search while a sibling model
|
|
727
|
+
* is fine on chat completions (#404).
|
|
728
|
+
*
|
|
729
|
+
* Only OpenAI-shaped wires may be selected; see MODEL_ADAPTER_OVERRIDE_ALLOWED.
|
|
730
|
+
* Absent or empty means the provider-wide `adapter` applies to everything, exactly
|
|
731
|
+
* as before.
|
|
732
|
+
*/
|
|
733
|
+
modelAdapters?: Record<string, string>;
|
|
721
734
|
baseUrl: string;
|
|
722
735
|
/**
|
|
723
736
|
* Optional relative resource path for key-auth openai-responses requests. Must start with `/`
|
|
@@ -930,6 +943,45 @@ export type CodexAccountMode = "direct" | "pool";
|
|
|
930
943
|
|
|
931
944
|
export const OPENAI_PROVIDER_TIER_VERSION = 2 as const;
|
|
932
945
|
|
|
946
|
+
/**
|
|
947
|
+
* Wires that a per-model `modelAdapters` override may select.
|
|
948
|
+
*
|
|
949
|
+
* Deliberately narrow: provider-specific adapters (cursor, kiro, google, ...) carry
|
|
950
|
+
* their own credential and base-URL semantics, so exposing them here would widen the
|
|
951
|
+
* auth boundary rather than pick a wire. Widening this set needs a per-adapter
|
|
952
|
+
* credential threat model first (#404).
|
|
953
|
+
*/
|
|
954
|
+
export const MODEL_ADAPTER_OVERRIDE_ALLOWED: ReadonlySet<string> = new Set([
|
|
955
|
+
"openai-chat",
|
|
956
|
+
"openai-responses",
|
|
957
|
+
]);
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Providers whose listed model ids must be driven over the Anthropic wire even when
|
|
961
|
+
* the provider's configured adapter says otherwise — the upstream only speaks
|
|
962
|
+
* Anthropic for these models.
|
|
963
|
+
*/
|
|
964
|
+
const ANTHROPIC_WIRE_MODELS: Record<string, ReadonlySet<string>> = {
|
|
965
|
+
"opencode-go": new Set(["minimax-m2.5", "minimax-m2.7", "minimax-m3"]),
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* True when the upstream speaks exactly one wire for this model, so a configured
|
|
970
|
+
* override must not apply.
|
|
971
|
+
*
|
|
972
|
+
* Deliberately independent of the provider's current adapter: the wire resolver runs
|
|
973
|
+
* more than once per request, and a check phrased as "pin differs from the current
|
|
974
|
+
* adapter" would pass on the first pass and then let the override win on the second.
|
|
975
|
+
*/
|
|
976
|
+
export function isWirePinnedModel(providerName: string, modelId: string): boolean {
|
|
977
|
+
return ANTHROPIC_WIRE_MODELS[providerName]?.has(modelId) ?? false;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** The wire a pinned model must use, or undefined when the model is not pinned. */
|
|
981
|
+
export function pinnedWireAdapter(providerName: string, modelId: string): string | undefined {
|
|
982
|
+
return isWirePinnedModel(providerName, modelId) ? "anthropic" : undefined;
|
|
983
|
+
}
|
|
984
|
+
|
|
933
985
|
export interface CodexAccount {
|
|
934
986
|
id: string;
|
|
935
987
|
email: string;
|
|
@@ -41,6 +41,11 @@ const QWEN38_ROUTEWAY_TEMPORARY: Cost4 = { input: 1.5, output: 5, cacheRead: 0.1
|
|
|
41
41
|
// Anthropic official list prices (USD / 1M tokens). Cache write uses the published 5-minute rate.
|
|
42
42
|
const CLAUDE_SONNET_46: Cost4 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
|
|
43
43
|
const CLAUDE_OPUS_46: Cost4 = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
|
|
44
|
+
// Opus 5 is priced from the maintainer's confirmation that it matches the previous
|
|
45
|
+
// Opus, not from a published Opus 5 page. Hence `verified-derived`, and a source
|
|
46
|
+
// string that states the provenance instead of pointing at ANTHROPIC_PRICING.
|
|
47
|
+
const CLAUDE_OPUS_5_DERIVED_SOURCE =
|
|
48
|
+
"user-confirmed: claude-opus-5 matches Claude Opus 4.6; no separate Anthropic Opus 5 price page verified";
|
|
44
49
|
const ANTHROPIC_PRICING = "https://platform.claude.com/docs/en/about-claude/pricing (official; 5m cache-write tier)";
|
|
45
50
|
|
|
46
51
|
const GEMINI_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-07-22); cacheWrite=0: storage is billed per-hour, not per-token";
|
|
@@ -54,6 +59,13 @@ const KIMI_PRICING = "https://platform.kimi.ai/docs/pricing (official table; cac
|
|
|
54
59
|
const QWEN38_ROUTEWAY_PRICING = "https://routeway.ai/models/qwen3.8-max-preview (temporary reseller proxy; NOT Alibaba Token Plan billing; cacheWrite unpublished -> 0)";
|
|
55
60
|
|
|
56
61
|
export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
62
|
+
// claude-opus-5 is exposed by three providers but absent from the jawcode bundle, so
|
|
63
|
+
// cost resolution returned null and the Logs `~$` column rendered an em dash. The
|
|
64
|
+
// model-level vendor fallback only searches jawcode metadata, never overlays, so one
|
|
65
|
+
// anthropic row would not cover cursor/kiro — each exposing provider needs its own.
|
|
66
|
+
{ provider: "anthropic", modelId: "claude-opus-5", cost4: CLAUDE_OPUS_46, source: CLAUDE_OPUS_5_DERIVED_SOURCE, verifiedAt: "2026-07-25", status: "verified-derived" },
|
|
67
|
+
{ provider: "cursor", modelId: "claude-opus-5", cost4: CLAUDE_OPUS_46, source: CLAUDE_OPUS_5_DERIVED_SOURCE, verifiedAt: "2026-07-25", status: "verified-derived" },
|
|
68
|
+
{ provider: "kiro", modelId: "claude-opus-5", cost4: CLAUDE_OPUS_46, source: CLAUDE_OPUS_5_DERIVED_SOURCE, verifiedAt: "2026-07-25", status: "verified-derived" },
|
|
57
69
|
// MiniMax M2.1 highspeed — published PAYG price (verified).
|
|
58
70
|
{ provider: "minimax", modelId: "MiniMax-M2.1-highspeed", cost4: MINIMAX_M21_HIGHSPEED, source: MINIMAX_PRICING, verifiedAt: "2026-07-20", status: "verified" },
|
|
59
71
|
{ provider: "minimax-cn", modelId: "MiniMax-M2.1-highspeed", cost4: MINIMAX_M21_HIGHSPEED, source: MINIMAX_PRICING, verifiedAt: "2026-07-20", status: "verified" },
|
|
@@ -3,6 +3,7 @@ import { getValidAccessToken } from "../oauth";
|
|
|
3
3
|
import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION } from "../oauth/anthropic";
|
|
4
4
|
import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "../adapters/client-fingerprint";
|
|
5
5
|
import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort";
|
|
6
|
+
import { redactSecretString } from "../lib/redact";
|
|
6
7
|
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
7
8
|
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
8
9
|
import type { WebSearchSource } from "./parse";
|
|
@@ -168,7 +169,8 @@ export async function runAnthropicWebSearch(
|
|
|
168
169
|
if (!res.ok) {
|
|
169
170
|
const t = await res.text().catch(() => "");
|
|
170
171
|
console.warn(`[web-search] anthropic sidecar HTTP ${res.status} for query "${query.slice(0, 80)}" (${Date.now() - t0}ms)`);
|
|
171
|
-
|
|
172
|
+
// Redact before surfacing: the body can echo auth headers/tokens (#398 review).
|
|
173
|
+
return { text: "", sources: [], error: `sidecar HTTP ${res.status}: ${redactSecretString(t.slice(0, 200))}` };
|
|
172
174
|
}
|
|
173
175
|
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
174
176
|
try {
|
package/src/web-search/index.ts
CHANGED
|
@@ -16,7 +16,13 @@ const DEFAULT_ANTHROPIC_SIDECAR_MODEL = "claude-sonnet-5";
|
|
|
16
16
|
// "tools cannot be used with reasoning.effort 'minimal'") — keeps the sidecar fast/cheap.
|
|
17
17
|
const DEFAULT_SIDECAR_REASONING = "low";
|
|
18
18
|
const DEFAULT_MAX_SEARCHES = 3;
|
|
19
|
-
|
|
19
|
+
// Per-search sidecar deadline. Lowered from 200_000 to 60_000 (#398): a hung
|
|
20
|
+
// hosted web_search used to run the full 200s, so the client cancelled first
|
|
21
|
+
// (turn 499) or the forced-answer routed iteration failed (502). Hosted-search
|
|
22
|
+
// p90 is ~43s, so 60s bounds hangs while leaving tail margin. `cfg.timeoutMs`
|
|
23
|
+
// still overrides. Distinct from DEFAULT_ROUTED_MODEL_STALL_TIMEOUT_MS below,
|
|
24
|
+
// which is the routed-model body-inactivity budget (unchanged).
|
|
25
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
20
26
|
const DEFAULT_ROUTED_MODEL_STALL_TIMEOUT_MS = 200_000;
|
|
21
27
|
const MAX_ROUTED_MODEL_STALL_TIMEOUT_MS = 2_147_483_647;
|
|
22
28
|
const STALL_MARGIN_SEC = 30;
|
package/src/web-search/loop.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { bridgeToResponsesSSE } from "../bridge";
|
|
|
5
5
|
import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
|
|
6
6
|
import { runAnthropicWebSearch } from "./anthropic-executor";
|
|
7
7
|
import { clearableDeadline } from "../lib/abort";
|
|
8
|
+
import { redactSecretString } from "../lib/redact";
|
|
8
9
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
9
10
|
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
10
11
|
import { formatWebSearchResults } from "./format-result";
|
|
@@ -424,9 +425,22 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
424
425
|
}
|
|
425
426
|
// F5: the anthropic sidecar authenticates with its own stored OAuth — it never touches the
|
|
426
427
|
// ChatGPT forward headers and must NOT record a Codex/OpenAI pool outcome.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
428
|
+
// #398: the executors are "never throws", but enforce the contract defensively so a future
|
|
429
|
+
// throw degrades to a failed tool result instead of aborting the whole turn. A genuine
|
|
430
|
+
// parent abort MUST stay 499 — the executors catch abort and RETURN {error}, so check
|
|
431
|
+
// signal.aborted both after the await and in the catch (a fulfilled {error} on an aborted
|
|
432
|
+
// signal would otherwise look like an ordinary degradable failure).
|
|
433
|
+
try {
|
|
434
|
+
outcome = backend === "anthropic" && anthropicSidecar
|
|
435
|
+
? await runAnthropicWebSearch(query, anthropicSidecar.providerName, anthropicSidecar.provider, settings, signal)
|
|
436
|
+
: await runWebSearch(query, hostedTool, forwardProvider!, selectedForwardHeaders, settings, signal, recordSidecarOutcome);
|
|
437
|
+
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
438
|
+
} catch (e) {
|
|
439
|
+
if (e instanceof LoopError) throw e;
|
|
440
|
+
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
441
|
+
// Unexpected executor throw: degrade this query to a failed tool result (redacted).
|
|
442
|
+
outcome = { text: "", sources: [], error: `sidecar failed: ${redactSecretString(e instanceof Error ? e.message : String(e))}` };
|
|
443
|
+
}
|
|
430
444
|
searchesExecuted++;
|
|
431
445
|
executedSearchCount++;
|
|
432
446
|
if (outcome.error) failedQueries.add(normalizeQuery(query));
|