@bitkyc08/opencodex 2.91.0 → 2.92.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/bin/ocx.mjs +37 -3
- package/gui/dist/assets/{App-nZ34cR5G.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-BuLy7hL1.js → Tray-BSsxznDm.js} +1 -1
- package/gui/dist/assets/index-BlFE_VB_.js +94 -0
- package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
- package/package.json +4 -3
- package/src/adapters/google-vertex-endpoint.ts +9 -1
- package/src/adapters/google-vertex-key-project.ts +223 -0
- package/src/adapters/google.ts +13 -3
- package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/request-strips.ts +60 -0
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/workbuddy-schema.ts +61 -0
- package/src/adapters/workbuddy.ts +68 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/chat/outbound.ts +11 -4
- package/src/cli/capabilities-agents-routing.ts +3 -3
- package/src/cli/codex-cli-update.ts +8 -0
- package/src/cli/combo-result.ts +6 -0
- package/src/cli/combo-stats.ts +3 -0
- package/src/cli/combo.ts +66 -42
- package/src/cli/launcher-context.ts +49 -2
- package/src/cli/link.ts +45 -32
- package/src/cli/provider.ts +2 -1
- package/src/cli/usage-compare.ts +68 -12
- package/src/client/link-ingress.ts +1 -1
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +79 -11
- package/src/client/link-relay.ts +74 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +20 -2
- package/src/client/runtime.ts +20 -2
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- package/src/codex/catalog/provider-models.ts +14 -8
- package/src/codex/chatgpt-refresh-failure.ts +6 -2
- package/src/codex/cli-install-provenance.ts +418 -0
- package/src/codex/cli-installation-targets.ts +5 -1
- package/src/codex/cli-update-plan.ts +43 -16
- package/src/codex/desktop-app/picker-freshness.ts +10 -8
- package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
- package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
- package/src/codex/pool-refresh-backoff.ts +3 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -0
- package/src/combos/jev-quota-activation.ts +5 -1
- package/src/combos/jev-quota-route.ts +1 -1
- package/src/combos/jev-quota.ts +9 -1
- package/src/combos/jev-session-pin-config.ts +51 -0
- package/src/combos/jev.ts +6 -0
- package/src/combos/types.ts +19 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +194 -122
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/translator-budget.ts +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- package/src/link/diagnostics.ts +193 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/status-projection.ts +39 -0
- package/src/link/supervisor.ts +45 -12
- package/src/link/tunnel-history.ts +109 -0
- package/src/link/tunnel-state.ts +80 -17
- package/src/messaging/remote-files.ts +24 -1
- package/src/oauth/generic-account-failover.ts +91 -7
- package/src/oauth/index.ts +15 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/store.ts +4 -1
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- package/src/providers/antigravity-quota-identity.ts +37 -0
- package/src/providers/base-url-choices.ts +8 -5
- package/src/providers/derive.ts +23 -3
- package/src/providers/model-discovery.ts +2 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/quota/account-cache.ts +3 -2
- package/src/providers/quota/antigravity.ts +25 -14
- package/src/providers/quota/report-cache.ts +13 -4
- package/src/providers/quota/vendor-probes-key.ts +60 -0
- package/src/providers/quota/workbuddy.ts +83 -0
- package/src/providers/quota-decision-antigravity.ts +64 -0
- package/src/providers/quota-decision-publication.ts +11 -0
- package/src/providers/quota-decision-snapshot.ts +21 -4
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +3 -1
- package/src/providers/registry/entries-core.ts +9 -0
- package/src/providers/registry/entries-extended.ts +20 -7
- package/src/providers/registry/model-ids.ts +3 -0
- package/src/providers/registry/model-seeds.ts +27 -28
- package/src/providers/registry/types.ts +19 -1
- package/src/providers/resolved-model-policy.ts +4 -4
- package/src/providers/routed-config.ts +15 -0
- package/src/providers/stale-model-roster-migration.ts +83 -15
- package/src/providers/workbuddy-credit-balance.ts +81 -0
- package/src/providers/workbuddy-models.ts +37 -0
- package/src/providers/workbuddy.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +18 -14
- package/src/router.ts +2 -0
- package/src/routing/analytics.ts +2 -3
- package/src/routing/fallback-policy.ts +3 -3
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native-reasoning.ts +10 -3
- package/src/server/chat-native.ts +13 -2
- package/src/server/claude-messages.ts +3 -0
- package/src/server/index/link-relay-sessions.ts +9 -5
- package/src/server/index/optional-listeners.ts +4 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- package/src/server/management/link-routes.ts +5 -2
- package/src/server/management/provider-routes.ts +1 -1
- package/src/server/port-reclaim.ts +51 -9
- package/src/server/request-log-conversation.ts +49 -0
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +10 -2
- package/src/server/responses/core-combo.ts +53 -31
- package/src/server/responses/core-lifetime.ts +2 -2
- package/src/server/responses/core-normalize.ts +6 -3
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +2 -1
- package/src/server/responses/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/request-prepare.ts +10 -5
- package/src/server/responses/request-transport.ts +25 -6
- package/src/server/responses/run-turn-execution.ts +2 -4
- package/src/server/responses/sidecar-execution.ts +2 -4
- package/src/server/responses-phase-inference.ts +267 -0
- package/src/server/sse-payload-rewrite.ts +14 -1
- package/src/service/definition.ts +20 -8
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +9 -1
- package/src/types/request.ts +1 -0
- package/src/types/wire.ts +1 -0
- package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/usage/jev-stats.ts +54 -14
- package/gui/dist/assets/index-DGXS51sp.js +0 -94
- package/gui/dist/assets/quota-summary-DO1Sx5TM.js +0 -1
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
* This rewrites one thing: a saved `models` list that is still byte-for-byte the roster
|
|
11
11
|
* this file names as `from`, on a provider that still carries the registry adapter, and
|
|
12
12
|
* only while the registry currently seeds `to`. A list the user changed does not match and
|
|
13
|
-
* is left alone. For each added id it also fills a per-model context window
|
|
14
|
-
* effort, but only inside a container the row already has: enrichment fills
|
|
15
|
-
* all-or-nothing, so creating one here would hide the rest of the registry seed.
|
|
13
|
+
* is left alone. For each added id it also fills a per-model context window, output limit
|
|
14
|
+
* and default effort, but only inside a container the row already has: enrichment fills
|
|
15
|
+
* those records all-or-nothing, so creating one here would hide the rest of the registry seed.
|
|
16
16
|
* Same shape and restraint as `stale-context-window-migration`.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { ALIBABA_INTL_TOKEN_PLAN_BASE_URL } from "./base-url-choices";
|
|
19
|
+
import { PROVIDER_REGISTRY, normalizedProviderEndpoint } from "./registry";
|
|
19
20
|
import {
|
|
21
|
+
ALIBABA_TOKEN_PLAN_MODELS,
|
|
22
|
+
ALIBABA_INTL_TOKEN_PLAN_MODELS,
|
|
23
|
+
ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS,
|
|
24
|
+
ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS,
|
|
20
25
|
MINIMAX_M31_DEFAULT_REASONING_EFFORT,
|
|
21
26
|
MINIMAX_M31_FLASH_PREVIEW,
|
|
22
27
|
MINIMAX_MODELS,
|
|
@@ -33,7 +38,18 @@ export interface StaleModelRoster {
|
|
|
33
38
|
to: readonly string[];
|
|
34
39
|
/** Per-model seed values for the added ids, written only into containers that exist. */
|
|
35
40
|
contextWindows?: Readonly<Record<string, number>>;
|
|
41
|
+
/**
|
|
42
|
+
* Custom catalog rows read the saved map directly, so an added id missing from a
|
|
43
|
+
* partial map would publish without the registry output limit.
|
|
44
|
+
*/
|
|
45
|
+
maxOutputTokens?: Readonly<Record<string, number>>;
|
|
36
46
|
defaultReasoningEfforts?: Readonly<Record<string, string>>;
|
|
47
|
+
/**
|
|
48
|
+
* For a provider whose saved URL may point elsewhere (pay-as-you-go or Custom), the
|
|
49
|
+
* destinations whose roster `to` describes. A row on any other URL keeps its list; a row
|
|
50
|
+
* with no saved URL uses the registry default and is refreshed.
|
|
51
|
+
*/
|
|
52
|
+
destinationBaseUrls?: readonly string[];
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
export interface StaleModelRosterProjection {
|
|
@@ -42,17 +58,45 @@ export interface StaleModelRosterProjection {
|
|
|
42
58
|
warnings: string[];
|
|
43
59
|
}
|
|
44
60
|
|
|
61
|
+
const ALIBABA_TOKEN_PLAN_MODELS_BEFORE_1017 = [
|
|
62
|
+
"qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash",
|
|
63
|
+
"deepseek-v4-pro", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "glm-5.2", "glm-5.3",
|
|
64
|
+
];
|
|
65
|
+
const ALIBABA_INTL_TOKEN_PLAN_MODELS_BEFORE_1017 = [
|
|
66
|
+
"qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
|
|
67
|
+
"deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "deepseek-v3.2",
|
|
68
|
+
"kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5",
|
|
69
|
+
"glm-5.2", "glm-5.3", "glm-5.1", "glm-5",
|
|
70
|
+
"MiniMax-M2.5",
|
|
71
|
+
];
|
|
72
|
+
|
|
45
73
|
/**
|
|
46
74
|
* MiniMax-M3.1-Flash-Preview (2026-09-30). MiniMax's /v1/models roster does not list the
|
|
47
75
|
* preview, so live discovery cannot add it; the catalog keeps it only when it is configured.
|
|
48
76
|
*/
|
|
49
|
-
export const STALE_MODEL_ROSTERS: readonly StaleModelRoster[] = [
|
|
50
|
-
provider
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
77
|
+
export const STALE_MODEL_ROSTERS: readonly StaleModelRoster[] = [
|
|
78
|
+
...["minimax", "minimax-cn"].map(provider => ({
|
|
79
|
+
provider,
|
|
80
|
+
from: MINIMAX_MODELS_BEFORE_M31,
|
|
81
|
+
to: MINIMAX_MODELS,
|
|
82
|
+
contextWindows: { [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_MODEL_CONTEXT_WINDOWS[MINIMAX_M31_FLASH_PREVIEW]! },
|
|
83
|
+
defaultReasoningEfforts: { [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_DEFAULT_REASONING_EFFORT },
|
|
84
|
+
})),
|
|
85
|
+
...[
|
|
86
|
+
{ provider: "alibaba-token-plan", from: ALIBABA_TOKEN_PLAN_MODELS_BEFORE_1017, to: ALIBABA_TOKEN_PLAN_MODELS },
|
|
87
|
+
// International honors pay-as-you-go and Custom URLs, which may still serve the retired ids.
|
|
88
|
+
// Beijing needs no guard: the router pins it to the registry Token Plan host.
|
|
89
|
+
{
|
|
90
|
+
provider: "alibaba-token-plan-intl", from: ALIBABA_INTL_TOKEN_PLAN_MODELS_BEFORE_1017, to: ALIBABA_INTL_TOKEN_PLAN_MODELS,
|
|
91
|
+
destinationBaseUrls: [ALIBABA_INTL_TOKEN_PLAN_BASE_URL],
|
|
92
|
+
},
|
|
93
|
+
].map(entry => ({
|
|
94
|
+
...entry,
|
|
95
|
+
contextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS,
|
|
96
|
+
maxOutputTokens: ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS,
|
|
97
|
+
defaultReasoningEfforts: { "deepseek-v4.1-flash-1017": "high" },
|
|
98
|
+
})),
|
|
99
|
+
];
|
|
56
100
|
|
|
57
101
|
function sameList(value: unknown, expected: readonly string[]): boolean {
|
|
58
102
|
return Array.isArray(value)
|
|
@@ -65,12 +109,24 @@ function registrySeeds(provider: string, adapter: unknown, roster: readonly stri
|
|
|
65
109
|
return entry !== undefined && entry.adapter === adapter && sameList(entry.models, roster);
|
|
66
110
|
}
|
|
67
111
|
|
|
68
|
-
function
|
|
69
|
-
if (!
|
|
112
|
+
function servesRoster(baseUrl: unknown, destinations: readonly string[] | undefined): boolean {
|
|
113
|
+
if (!destinations || baseUrl === undefined || baseUrl === "") return true;
|
|
114
|
+
if (typeof baseUrl !== "string") return false;
|
|
115
|
+
const saved = normalizedProviderEndpoint(baseUrl);
|
|
116
|
+
return destinations.some(url => normalizedProviderEndpoint(url) === saved);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function fillExisting<T>(container: Record<string, T> | undefined, values: Readonly<Record<string, T>> | undefined, ids: readonly string[]): boolean {
|
|
120
|
+
if (!container || !values) return false;
|
|
121
|
+
let filled = false;
|
|
70
122
|
for (const id of ids) {
|
|
71
123
|
const value = values[id];
|
|
72
|
-
if (value !== undefined && !Object.prototype.hasOwnProperty.call(container, id))
|
|
124
|
+
if (value !== undefined && !Object.prototype.hasOwnProperty.call(container, id)) {
|
|
125
|
+
container[id] = value;
|
|
126
|
+
filled = true;
|
|
127
|
+
}
|
|
73
128
|
}
|
|
129
|
+
return filled;
|
|
74
130
|
}
|
|
75
131
|
|
|
76
132
|
/** Pure projection. The caller decides whether to persist. */
|
|
@@ -85,10 +141,22 @@ export function projectStaleModelRosters(
|
|
|
85
141
|
const prov = config.providers?.[entry.provider];
|
|
86
142
|
if (!prov) continue;
|
|
87
143
|
if (!registrySeeds(entry.provider, prov.adapter, entry.to)) continue;
|
|
88
|
-
if (!
|
|
144
|
+
if (!servesRoster(prov.baseUrl, entry.destinationBaseUrls)) continue;
|
|
89
145
|
const added = entry.to.filter(id => !entry.from.includes(id));
|
|
146
|
+
if (sameList(prov.models, entry.to)) {
|
|
147
|
+
// A row refreshed before output limits joined the refresh still has a partial map.
|
|
148
|
+
// Only that record is revisited: the earlier pass already filled windows and efforts,
|
|
149
|
+
// so refilling them could restore a key the operator has since removed.
|
|
150
|
+
if (fillExisting(prov.modelMaxOutputTokens, entry.maxOutputTokens, added)) {
|
|
151
|
+
changed = true;
|
|
152
|
+
warnings.push(`filled output limits for ${added.join(", ")} on the saved "${entry.provider}" row.`);
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!sameList(prov.models, entry.from)) continue;
|
|
90
157
|
prov.models = [...entry.to];
|
|
91
158
|
fillExisting(prov.modelContextWindows, entry.contextWindows, added);
|
|
159
|
+
fillExisting(prov.modelMaxOutputTokens, entry.maxOutputTokens, added);
|
|
92
160
|
fillExisting(prov.modelDefaultReasoningEfforts, entry.defaultReasoningEfforts, added);
|
|
93
161
|
changed = true;
|
|
94
162
|
warnings.push(
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface WorkBuddyCreditAmount {
|
|
2
|
+
total: number;
|
|
3
|
+
used: number;
|
|
4
|
+
remaining: number;
|
|
5
|
+
frozen: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const WORKBUDDY_CREDIT_KINDS = ["plan_base", "plan_bonus", "addon", "benefit", "other"] as const;
|
|
9
|
+
export type WorkBuddyCreditKind = typeof WORKBUDDY_CREDIT_KINDS[number];
|
|
10
|
+
|
|
11
|
+
/** Display-only credits; they are neither USD nor provider-wide model eligibility. */
|
|
12
|
+
export interface WorkBuddyCreditBalance extends WorkBuddyCreditAmount {
|
|
13
|
+
groups: Array<WorkBuddyCreditAmount & { kind: WorkBuddyCreditKind }>;
|
|
14
|
+
expiring?: { amount: number; at: number };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function workBuddyRecord(value: unknown): Record<string, unknown> {
|
|
18
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function workBuddyCapacity(value: unknown): number | undefined {
|
|
22
|
+
if (typeof value !== "number" && !(typeof value === "string" && /^\d+(?:\.\d+)?$/.test(value))) return undefined;
|
|
23
|
+
const number = Number(value);
|
|
24
|
+
return Number.isFinite(number) && number >= 0 && number <= Number.MAX_SAFE_INTEGER ? number : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function amount(value: unknown): WorkBuddyCreditAmount | undefined {
|
|
28
|
+
const row = workBuddyRecord(value);
|
|
29
|
+
const total = workBuddyCapacity(row.total), used = workBuddyCapacity(row.used);
|
|
30
|
+
const remaining = workBuddyCapacity(row.remaining), frozen = workBuddyCapacity(row.frozen);
|
|
31
|
+
if (total === undefined || used === undefined || remaining === undefined || frozen === undefined) return undefined;
|
|
32
|
+
return { total, used, remaining, frozen };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Narrow wire/cache input before rendering, without admitting account or package identifiers. */
|
|
36
|
+
export function normalizeWorkBuddyCredits(value: unknown): WorkBuddyCreditBalance | undefined {
|
|
37
|
+
const row = workBuddyRecord(value), balance = amount(value);
|
|
38
|
+
if (!balance || !Array.isArray(row.groups) || row.groups.length > WORKBUDDY_CREDIT_KINDS.length) return undefined;
|
|
39
|
+
const groups: WorkBuddyCreditBalance["groups"] = [];
|
|
40
|
+
const seen = new Set<string>();
|
|
41
|
+
for (const raw of row.groups) {
|
|
42
|
+
const group = workBuddyRecord(raw), quantities = amount(group);
|
|
43
|
+
const kind = WORKBUDDY_CREDIT_KINDS.find(kind => kind === group.kind);
|
|
44
|
+
if (!quantities || !kind || seen.has(kind)) return undefined;
|
|
45
|
+
seen.add(kind); groups.push({ kind, ...quantities });
|
|
46
|
+
}
|
|
47
|
+
const expiring = workBuddyRecord(row.expiring), quantity = workBuddyCapacity(expiring.amount);
|
|
48
|
+
const at = typeof expiring.at === "number" && expiring.at > 0 && Number.isFinite(new Date(expiring.at).getTime()) ? expiring.at : undefined;
|
|
49
|
+
return { ...balance, groups, ...(quantity !== undefined && quantity > 0 && at !== undefined ? { expiring: { amount: quantity, at } } : {}) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function packageAmount(row: Record<string, unknown>): WorkBuddyCreditAmount | undefined {
|
|
53
|
+
const total = workBuddyCapacity(row.CycleTotalCapacity), used = workBuddyCapacity(row.CycleUsedCapacity);
|
|
54
|
+
const frozen = row.CycleFrozenCapacity === undefined ? 0 : workBuddyCapacity(row.CycleFrozenCapacity);
|
|
55
|
+
if (total === undefined || used === undefined || frozen === undefined) return undefined;
|
|
56
|
+
const remaining = row.CycleRemainCapacity === undefined ? Math.max(0, total - used - frozen) : workBuddyCapacity(row.CycleRemainCapacity);
|
|
57
|
+
return remaining === undefined ? undefined : { total, used, remaining, frozen };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseWorkBuddyCredits(value: unknown): WorkBuddyCreditBalance | undefined {
|
|
61
|
+
const summary = workBuddyRecord(value);
|
|
62
|
+
if (!Array.isArray(summary.Packages) || summary.Packages.length > 512) return undefined;
|
|
63
|
+
const balance: WorkBuddyCreditBalance = { total: 0, used: 0, remaining: 0, frozen: 0, groups: [] };
|
|
64
|
+
for (const raw of summary.Packages) {
|
|
65
|
+
const quantities = packageAmount(workBuddyRecord(raw));
|
|
66
|
+
if (!quantities) return undefined;
|
|
67
|
+
for (const key of ["total", "used", "remaining", "frozen"] as const) balance[key] += quantities[key];
|
|
68
|
+
}
|
|
69
|
+
// CreditGroups is a breakdown of Packages, not another balance to add to it.
|
|
70
|
+
if (Array.isArray(summary.CreditGroups) && summary.CreditGroups.length <= 16) {
|
|
71
|
+
for (const raw of summary.CreditGroups) {
|
|
72
|
+
const row = workBuddyRecord(raw), quantities = packageAmount(row);
|
|
73
|
+
if (!quantities) continue;
|
|
74
|
+
const kind = WORKBUDDY_CREDIT_KINDS.find(kind => kind === row.Kind) ?? "other";
|
|
75
|
+
const existing = balance.groups.find(group => group.kind === kind);
|
|
76
|
+
if (existing) for (const key of ["total", "used", "remaining", "frozen"] as const) existing[key] += quantities[key];
|
|
77
|
+
else balance.groups.push({ kind, ...quantities });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return normalizeWorkBuddyCredits(balance);
|
|
81
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isValidModelDiscoveryModelId } from "./model-discovery-limits";
|
|
2
|
+
import type { ProviderModelItemsResult, ProviderModelsApiItem } from "./model-discovery";
|
|
3
|
+
|
|
4
|
+
function record(value: unknown): Record<string, unknown> {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** The product catalog contains non-chat models too; only the CLI agent's roster is routable. */
|
|
9
|
+
export function extractWorkBuddyModels(value: unknown, limit: number): ProviderModelItemsResult {
|
|
10
|
+
const envelope = record(value), data = record(envelope.data);
|
|
11
|
+
if ((envelope.code !== 0 && envelope.code !== "0") || !Array.isArray(data.agents) || !Array.isArray(data.models)) return { ok: false, reason: "invalid_shape" };
|
|
12
|
+
if (data.models.length > limit || data.agents.length > limit) return { ok: false, reason: "too_many_models" };
|
|
13
|
+
const agent = data.agents.map(record).find(a => a.name === "cli");
|
|
14
|
+
if (!Array.isArray(agent?.models)) return { ok: false, reason: "invalid_shape" };
|
|
15
|
+
if (agent.models.length > limit) return { ok: false, reason: "too_many_models" };
|
|
16
|
+
const definitions = new Map(data.models.map(raw => { const row = record(raw); return [row.id, row] as const; }));
|
|
17
|
+
const items: ProviderModelsApiItem[] = [], seen = new Set<string>();
|
|
18
|
+
for (const id of agent.models) {
|
|
19
|
+
if (!isValidModelDiscoveryModelId(id)) return { ok: false, reason: "invalid_shape" };
|
|
20
|
+
if (seen.has(id)) continue;
|
|
21
|
+
seen.add(id);
|
|
22
|
+
const row = definitions.get(id) ?? {}, reasoning = record(row.reasoning);
|
|
23
|
+
const rawEfforts = Array.isArray(reasoning.supportedEfforts) ? reasoning.supportedEfforts
|
|
24
|
+
: row.supportsReasoning === true ? ["low", "high", "max"] : [];
|
|
25
|
+
const efforts = rawEfforts.filter(e => typeof e === "string" && ["low", "medium", "high", "xhigh", "max"].includes(e));
|
|
26
|
+
if (efforts.length && row.onlyReasoning !== true && reasoning.canDisableThinking !== false) efforts.unshift("none");
|
|
27
|
+
const positive = (v: unknown) => typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : undefined;
|
|
28
|
+
items.push({
|
|
29
|
+
id, ...(typeof row.name === "string" && row.name.length <= 256 ? { name: row.name } : {}),
|
|
30
|
+
context_length: positive(row.maxInputTokens), max_input_tokens: positive(row.maxInputTokens),
|
|
31
|
+
max_output_tokens: positive(row.maxOutputTokens), reasoning_efforts: efforts,
|
|
32
|
+
// WorkBuddy AI filters images from GLM requests despite advertising supportsImages.
|
|
33
|
+
input_modalities: row.supportsImages === true && !id.startsWith("glm-") ? ["text", "image"] : ["text"],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return { ok: true, items, rawCount: agent.models.length };
|
|
37
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { WorkBuddyOAuthMetadata } from "../oauth/types";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
export const WORKBUDDY_ORIGIN = "https://www.workbuddy.ai";
|
|
5
|
+
export const WORKBUDDY_BASE_URL = `${WORKBUDDY_ORIGIN}/v2`;
|
|
6
|
+
export const WORKBUDDY_CONFIG_URL = `${WORKBUDDY_ORIGIN}/v3/config`;
|
|
7
|
+
export const WORKBUDDY_USER_AGENT = "WorkBuddy/5.5.6";
|
|
8
|
+
|
|
9
|
+
export function workBuddyCatalogIdentity(access: string, metadata: WorkBuddyOAuthMetadata | undefined): string {
|
|
10
|
+
return createHash("sha256").update(JSON.stringify([access, metadata])).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function workBuddyHeaderValue(value: unknown, max = 1024): value is string {
|
|
14
|
+
return typeof value === "string" && value.length > 0 && value.length <= max && /^[\x21-\x7e]+$/.test(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeWorkBuddyMetadata(value: unknown): WorkBuddyOAuthMetadata | undefined {
|
|
18
|
+
if (!value || typeof value !== "object") return undefined;
|
|
19
|
+
const v = value as Partial<WorkBuddyOAuthMetadata>;
|
|
20
|
+
if (!workBuddyHeaderValue(v.uid) || !workBuddyHeaderValue(v.domain)) return undefined;
|
|
21
|
+
if (v.enterpriseId !== undefined && !workBuddyHeaderValue(v.enterpriseId)) return undefined;
|
|
22
|
+
return {
|
|
23
|
+
uid: v.uid, domain: v.domain,
|
|
24
|
+
...(v.enterpriseId ? { enterpriseId: v.enterpriseId } : {}),
|
|
25
|
+
...(typeof v.refreshExpiresAt === "number" && Number.isFinite(v.refreshExpiresAt) && v.refreshExpiresAt > 0
|
|
26
|
+
? { refreshExpiresAt: v.refreshExpiresAt } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function assertWorkBuddyDestination(baseUrl: string): void {
|
|
31
|
+
const u = new URL(baseUrl);
|
|
32
|
+
if (u.origin !== WORKBUDDY_ORIGIN || u.username || u.password || u.search || u.hash
|
|
33
|
+
|| !["", "/v2", "/v2/chat/completions"].includes(u.pathname.replace(/\/+$/, ""))) {
|
|
34
|
+
throw new Error("WorkBuddy AI credentials require https://www.workbuddy.ai/v2");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function workBuddyHeaders(access: string | undefined, metadata?: WorkBuddyOAuthMetadata): Record<string, string> {
|
|
39
|
+
const headers: Record<string, string> = {
|
|
40
|
+
"User-Agent": WORKBUDDY_USER_AGENT,
|
|
41
|
+
"X-Product": "SaaS", "X-IDE-Type": "WorkBuddy", "X-IDE-Name": "WorkBuddy",
|
|
42
|
+
"X-IDE-Version": "5.5.6", "X-Requested-With": "XMLHttpRequest",
|
|
43
|
+
"X-Agent-Intent": "craft", "X-Agent-Purpose": "conversation", "X-Agent-Type": "main",
|
|
44
|
+
};
|
|
45
|
+
if (access) {
|
|
46
|
+
if (!workBuddyHeaderValue(access, 32768)) throw new Error("Invalid WorkBuddy AI access token");
|
|
47
|
+
headers.Authorization = `Bearer ${access}`;
|
|
48
|
+
}
|
|
49
|
+
if (metadata) {
|
|
50
|
+
const valid = normalizeWorkBuddyMetadata(metadata);
|
|
51
|
+
if (!valid) throw new Error("WorkBuddy AI account identity unavailable; sign in again");
|
|
52
|
+
headers["X-User-Id"] = valid.uid;
|
|
53
|
+
headers["X-Domain"] = valid.domain;
|
|
54
|
+
if (valid.enterpriseId) {
|
|
55
|
+
headers["X-Enterprise-Id"] = valid.enterpriseId;
|
|
56
|
+
headers["X-Tenant-Id"] = valid.enterpriseId;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return headers;
|
|
60
|
+
}
|
|
@@ -143,8 +143,9 @@ function tupleForServingIdentity(
|
|
|
143
143
|
|
|
144
144
|
function keyFor(callId: string, scope: OcxReasoningReplayScopeRef | undefined): string | undefined {
|
|
145
145
|
const identity = tupleForIdentity(scope?.current);
|
|
146
|
-
if (!nonEmpty(callId) || !nonEmpty(scope?.clientThreadId) || !identity) return undefined;
|
|
146
|
+
if (!nonEmpty(callId) || !nonEmpty(scope?.clientPrincipalId) || !nonEmpty(scope.clientThreadId) || !identity) return undefined;
|
|
147
147
|
return JSON.stringify([
|
|
148
|
+
scope.clientPrincipalId,
|
|
148
149
|
scope.clientThreadId,
|
|
149
150
|
...identity,
|
|
150
151
|
callId,
|
|
@@ -166,18 +167,21 @@ function sweepExpiredServingIdentities(at: number): void {
|
|
|
166
167
|
|
|
167
168
|
function servingIdentityFor(
|
|
168
169
|
scope: OcxReasoningReplayScopeRef | undefined,
|
|
169
|
-
): {
|
|
170
|
+
): { key: string; identity: string } | undefined {
|
|
170
171
|
const threadId = scope?.clientThreadId;
|
|
171
172
|
const identityTuple = tupleForServingIdentity(scope?.current);
|
|
172
173
|
if (!nonEmpty(threadId) || !identityTuple) return undefined;
|
|
173
|
-
|
|
174
|
+
// Preserve physical safeguards for keyless callers in a separate namespace. A caller's
|
|
175
|
+
// thread name cannot select an authenticated principal's serving or rejection history.
|
|
176
|
+
const key = JSON.stringify([nonEmpty(scope?.clientPrincipalId) ? scope.clientPrincipalId : null, threadId]);
|
|
177
|
+
return { key, identity: JSON.stringify(identityTuple) };
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
function opaqueBlobRejectionKeyFor(
|
|
177
181
|
scope: OcxReasoningReplayScopeRef | undefined,
|
|
178
182
|
): string | undefined {
|
|
179
183
|
const current = servingIdentityFor(scope);
|
|
180
|
-
return current ? JSON.stringify([current.
|
|
184
|
+
return current ? JSON.stringify([current.key, current.identity]) : undefined;
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
function deleteOpaqueBlobRejection(key: string): void {
|
|
@@ -224,7 +228,7 @@ export function reasoningReplayServingIdentityChange(
|
|
|
224
228
|
if (!current) return undefined;
|
|
225
229
|
const at = now();
|
|
226
230
|
sweepExpiredServingIdentities(at);
|
|
227
|
-
const previous = servingIdentities.get(current.
|
|
231
|
+
const previous = servingIdentities.get(current.key);
|
|
228
232
|
if (previous === undefined || previous.identity === current.identity) return undefined;
|
|
229
233
|
const before = JSON.parse(previous.identity) as ReasoningReplayIdentityTuple;
|
|
230
234
|
const after = JSON.parse(current.identity) as ReasoningReplayIdentityTuple;
|
|
@@ -254,7 +258,7 @@ export function reasoningReplayItemStoreChanged(
|
|
|
254
258
|
if (!current) return false;
|
|
255
259
|
const at = now();
|
|
256
260
|
sweepExpiredServingIdentities(at);
|
|
257
|
-
const previous = servingIdentities.get(current.
|
|
261
|
+
const previous = servingIdentities.get(current.key);
|
|
258
262
|
if (previous === undefined || previous.identity === current.identity) return false;
|
|
259
263
|
const storeOwner = (identity: string): string => {
|
|
260
264
|
const [, destination, , , credential] = JSON.parse(identity) as ReasoningReplayIdentityTuple;
|
|
@@ -271,16 +275,16 @@ export function commitReasoningReplayServingIdentity(
|
|
|
271
275
|
if (!current) return;
|
|
272
276
|
const at = now();
|
|
273
277
|
sweepExpiredServingIdentities(at);
|
|
274
|
-
const previous = servingIdentities.get(current.
|
|
275
|
-
const {
|
|
276
|
-
const bytes = Buffer.byteLength(JSON.stringify([
|
|
278
|
+
const previous = servingIdentities.get(current.key);
|
|
279
|
+
const { key, identity } = current;
|
|
280
|
+
const bytes = Buffer.byteLength(JSON.stringify([key, identity]), "utf8");
|
|
277
281
|
if (bytes > MAX_TOTAL_BYTES) {
|
|
278
|
-
deleteServingIdentity(
|
|
282
|
+
deleteServingIdentity(key);
|
|
279
283
|
return;
|
|
280
284
|
}
|
|
281
285
|
|
|
282
|
-
if (previous) deleteServingIdentity(
|
|
283
|
-
servingIdentities.set(
|
|
286
|
+
if (previous) deleteServingIdentity(key);
|
|
287
|
+
servingIdentities.set(key, { identity, bytes, at });
|
|
284
288
|
servingIdentityTotalBytes += bytes;
|
|
285
289
|
while (
|
|
286
290
|
(servingIdentityTotalBytes > MAX_TOTAL_BYTES || servingIdentities.size > MAX_ENTRIES)
|
|
@@ -517,13 +521,13 @@ export function reasoningReplayKeyCredentialIdentity(
|
|
|
517
521
|
return reasoningReplayCredentialIdentity("key", apiKey, provider.headers);
|
|
518
522
|
}
|
|
519
523
|
|
|
520
|
-
/**
|
|
524
|
+
/** Bind physical identity independently of the principal required for plaintext cache access. */
|
|
521
525
|
export function bindReasoningReplayScope(
|
|
522
526
|
scope: OcxReasoningReplayScopeRef | undefined,
|
|
523
527
|
identity: OcxReasoningReplayIdentity | undefined,
|
|
524
528
|
): void {
|
|
525
529
|
if (!scope) return;
|
|
526
|
-
if (!identity || !
|
|
530
|
+
if (!identity || !nonEmpty(scope.clientThreadId) || !tupleForIdentity(identity)) {
|
|
527
531
|
delete scope.current;
|
|
528
532
|
return;
|
|
529
533
|
}
|
package/src/router.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec";
|
|
34
34
|
import { effectiveProviderAliasDecision, resolveModelAlias } from "./providers/default-aliases";
|
|
35
35
|
import { resolveBlockedModelRedirect } from "./lib/shadow-call";
|
|
36
|
+
import { workBuddyCatalogIdentity } from "./providers/workbuddy";
|
|
36
37
|
import { getRoutingCached } from "./codex/model-cache";
|
|
37
38
|
import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
|
|
38
39
|
import {
|
|
@@ -177,6 +178,7 @@ export function knownModelIdsForProvider(
|
|
|
177
178
|
if (!account || account.needsReauth || !Number.isFinite(account.credential.expires)
|
|
178
179
|
|| account.credential.expires <= Date.now()) return undefined;
|
|
179
180
|
key = account.credential.access;
|
|
181
|
+
if (provName === "workbuddy-ai") return workBuddyCatalogIdentity(key, account.credential.workbuddy);
|
|
180
182
|
if (routed.adapter === "devin") destination = validateDevinApiBaseUrl(account.credential.apiBaseUrl) ?? routed.baseUrl;
|
|
181
183
|
}
|
|
182
184
|
if (!key) return undefined;
|
package/src/routing/analytics.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* sample for the full history.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type
|
|
14
|
+
import { normalizePersistedUsageRow, type PersistedUsageEntry } from "../usage/log";
|
|
15
15
|
import { computeEntryCost } from "../usage/summary";
|
|
16
16
|
import { openRequestHistoryIndex, requestHistoryDb } from "./history/indexer";
|
|
17
17
|
|
|
@@ -141,8 +141,7 @@ function classifyRow(row: ScannedRow): "success" | "failure" | "cancelled" {
|
|
|
141
141
|
|
|
142
142
|
function parseEntry(rowJson: string): PersistedUsageEntry | null {
|
|
143
143
|
try {
|
|
144
|
-
|
|
145
|
-
return parsed && typeof parsed === "object" && typeof parsed.requestId === "string" ? parsed : null;
|
|
144
|
+
return normalizePersistedUsageRow(JSON.parse(rowJson)) ?? null;
|
|
146
145
|
} catch {
|
|
147
146
|
return null;
|
|
148
147
|
}
|
|
@@ -192,9 +192,9 @@ export type FallbackGuardResult =
|
|
|
192
192
|
ok: true;
|
|
193
193
|
kind: OcxFallbackKind | null;
|
|
194
194
|
/**
|
|
195
|
-
* Record the hop as taken
|
|
196
|
-
*
|
|
197
|
-
*
|
|
195
|
+
* Record the hop as taken at the site's admission boundary (body rewrite, send-budget
|
|
196
|
+
* reservation, or accepted candidate). This does not prove upstream contact or billing;
|
|
197
|
+
* later preparation may still refuse. Idempotent; a no-op for a same-target retry.
|
|
198
198
|
*/
|
|
199
199
|
commit: () => void;
|
|
200
200
|
}
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -1195,6 +1195,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
1195
1195
|
desktopExecutor: "redacted",
|
|
1196
1196
|
unsafeAllowNativeLocalExec: "editor",
|
|
1197
1197
|
nativeLocalExec: "editor",
|
|
1198
|
+
messagePhaseInference: "editor",
|
|
1198
1199
|
tlsProfile: "editor",
|
|
1199
1200
|
} as const satisfies Record<keyof OcxProviderConfig, ProviderConfigFieldPolicy>;
|
|
1200
1201
|
|
|
@@ -40,6 +40,9 @@ import { evidenceFromBody } from "../routing/request-evidence";
|
|
|
40
40
|
import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
41
41
|
import { resolveOpenCodeGoTransport } from "../providers/opencode-go-transport";
|
|
42
42
|
import {
|
|
43
|
+
assignNativeLogConversationId,
|
|
44
|
+
fillHermesLogConversationFallback,
|
|
45
|
+
nativeLogConversationIdFromHeaders,
|
|
43
46
|
getOrAllocateRequestSessionLane,
|
|
44
47
|
linkRequestSessionLane,
|
|
45
48
|
} from "./request-log-conversation";
|
|
@@ -69,7 +72,7 @@ import {
|
|
|
69
72
|
assertRouteAllowedByScope,
|
|
70
73
|
resolveAdmissionModelScope,
|
|
71
74
|
} from "./admission-model-scope";
|
|
72
|
-
import type
|
|
75
|
+
import { resolveContextPrincipal, type DataPlaneAdmission } from "./auth-cors";
|
|
73
76
|
import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission";
|
|
74
77
|
import {
|
|
75
78
|
createTranslatorBudget,
|
|
@@ -114,6 +117,10 @@ export async function handleChatCompletions(
|
|
|
114
117
|
logCtx: RequestLogContext,
|
|
115
118
|
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission },
|
|
116
119
|
): Promise<Response> {
|
|
120
|
+
// Set before routing so pre-dispatch refusals stay correlated. A Hermes-only id is marked as a
|
|
121
|
+
// fallback, so a native id resolved later on the Responses bridge still replaces it.
|
|
122
|
+
assignNativeLogConversationId(logCtx, nativeLogConversationIdFromHeaders(req.headers));
|
|
123
|
+
fillHermesLogConversationFallback(logCtx, req.headers);
|
|
117
124
|
const translatorBudget = createTranslatorBudget();
|
|
118
125
|
try {
|
|
119
126
|
let response = await handleChatCompletionsWithBudget(req, config, logCtx, translatorBudget, logIds);
|
|
@@ -420,6 +427,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
420
427
|
// Unresolved combos are checked after their concrete child route is selected in Responses.
|
|
421
428
|
if (settledRoute && !settledRoute.combo && isCanonicalOpenAiForwardProvider(settledRoute.provider)
|
|
422
429
|
&& isCodexReserveHelperUnsupported(config, settledRoute.modelId, logIds?.admission, visionDescribeTerminal)) {
|
|
430
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
|
|
423
431
|
return chatCompletionsErrorResponse(400, CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, "invalid_request_error");
|
|
424
432
|
}
|
|
425
433
|
const nativeCallerAuth = captureExplicitOpenAiCallerAuth(req.headers, config);
|
|
@@ -498,6 +506,8 @@ async function handleChatCompletionsWithBudget(
|
|
|
498
506
|
// #1686: the Chat surface translates its body and replays here, so the admission fact has
|
|
499
507
|
// to ride along or a bearer-admitted Chat caller would still be refused by Direct.
|
|
500
508
|
...(logIds?.admission ? { admission: logIds.admission } : {}),
|
|
509
|
+
// The bridge request carries no caller credential, so resolve the replay principal here.
|
|
510
|
+
replayPrincipal: { clientPrincipalId: resolveContextPrincipal(req, config, logIds?.admission) },
|
|
501
511
|
abortSignal: req.signal,
|
|
502
512
|
// Body is Responses-shaped by now, but the client spoke Chat Completions.
|
|
503
513
|
inboundWire: "chat",
|
|
@@ -512,6 +522,9 @@ async function handleChatCompletionsWithBudget(
|
|
|
512
522
|
...(envelope && nativeChatCombos ? {
|
|
513
523
|
protocolSource: createNativeChatComboSource({
|
|
514
524
|
req, config, envelope, requestedModel, requestedStream: stream, translatorBudget,
|
|
525
|
+
// Children run without logIds; the request's admission rides the source so their
|
|
526
|
+
// replay scope resolves the same caller principal as every other surface.
|
|
527
|
+
...(logIds?.admission ? { admission: logIds.admission } : {}),
|
|
515
528
|
}),
|
|
516
529
|
} : {}),
|
|
517
530
|
// Terminal vision-describe marker (roadmap 180): the bridge rebuilds
|
|
@@ -7,11 +7,17 @@ import { reasoningReplayConversationIdFromResponsesRequest, sessionIdHeaderFromR
|
|
|
7
7
|
|
|
8
8
|
type Rec = Record<string, unknown>;
|
|
9
9
|
|
|
10
|
-
export function nativeChatReplayScope(
|
|
10
|
+
export function nativeChatReplayScope(
|
|
11
|
+
headers: Headers,
|
|
12
|
+
clientPrincipalId: string | undefined,
|
|
13
|
+
): OcxReasoningReplayScopeRef | undefined {
|
|
11
14
|
const clientThreadId = reasoningReplayConversationIdFromResponsesRequest({
|
|
12
15
|
threadIdHeader: headers.get("thread-id"), sessionIdHeader: sessionIdHeaderFromRequest(headers),
|
|
13
16
|
});
|
|
14
|
-
return clientThreadId ? {
|
|
17
|
+
return clientThreadId ? {
|
|
18
|
+
clientThreadId,
|
|
19
|
+
...(clientPrincipalId ? { clientPrincipalId } : {}),
|
|
20
|
+
} : undefined;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
/** Rebind before EVERY build: a key rotation must not retrieve another credential's reasoning. */
|
|
@@ -42,7 +48,8 @@ export function nativeChatReasoningCapture(
|
|
|
42
48
|
const maxBytes = 256 * 1024;
|
|
43
49
|
let bytes = 0;
|
|
44
50
|
let callCount = 0;
|
|
45
|
-
let disabled = !scope?.
|
|
51
|
+
let disabled = !scope?.clientPrincipalId || !scope.current
|
|
52
|
+
|| !chatReasoningReplayPolicy(provider, modelId).preserve;
|
|
46
53
|
const discard = () => {
|
|
47
54
|
budget.releaseRetained(bytes, accounting);
|
|
48
55
|
bytes = 0;
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "../lib/errors";
|
|
25
25
|
import { reportDispatchSends, type RequestExecutionBudget, type SingleUseDispatchPermit } from "../lib/request-execution-budget";
|
|
26
26
|
import type { AdmissionLease } from "../lib/admission";
|
|
27
|
+
import { resolveContextPrincipal, type DataPlaneAdmission } from "./auth-cors";
|
|
27
28
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
28
29
|
import { redactSecretString } from "../lib/redact";
|
|
29
30
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
@@ -179,7 +180,7 @@ export interface HandleNativeChatOptions {
|
|
|
179
180
|
req: Request;
|
|
180
181
|
config: OcxConfig;
|
|
181
182
|
logCtx: RequestLogContext;
|
|
182
|
-
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease };
|
|
183
|
+
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission };
|
|
183
184
|
route: RouteResult;
|
|
184
185
|
chatBody: Rec;
|
|
185
186
|
requestedModel: string;
|
|
@@ -209,6 +210,12 @@ export interface NativeChatExecution extends HandleNativeChatOptions {
|
|
|
209
210
|
onFirstOutput?: () => void;
|
|
210
211
|
/** The lease a streamed body holds; defaults to `logIds.turnAdmissionLease`. */
|
|
211
212
|
turnAdmissionLease?: AdmissionLease;
|
|
213
|
+
/**
|
|
214
|
+
* The request's resolved data-plane admission, when the caller carries it without `logIds`.
|
|
215
|
+
* A combo child runs on the parent's log row, so it receives the admitted identity here or
|
|
216
|
+
* its reasoning replay scope can never establish a caller principal (PF-07).
|
|
217
|
+
*/
|
|
218
|
+
admission?: DataPlaneAdmission;
|
|
212
219
|
}
|
|
213
220
|
|
|
214
221
|
/**
|
|
@@ -246,6 +253,8 @@ export function createNativeChatComboSource(input: {
|
|
|
246
253
|
requestedModel: string;
|
|
247
254
|
requestedStream: boolean;
|
|
248
255
|
translatorBudget: TranslatorBudget;
|
|
256
|
+
/** The request's data-plane admission; native children inherit it for replay scope. */
|
|
257
|
+
admission?: DataPlaneAdmission;
|
|
249
258
|
}): ComboProtocolSource {
|
|
250
259
|
return {
|
|
251
260
|
inbound: "chat",
|
|
@@ -264,6 +273,7 @@ export function createNativeChatComboSource(input: {
|
|
|
264
273
|
comboDispatchPermit: child.comboDispatchPermit,
|
|
265
274
|
onFirstOutput: child.onFirstOutput,
|
|
266
275
|
...(child.turnAdmissionLease ? { turnAdmissionLease: child.turnAdmissionLease } : {}),
|
|
276
|
+
...(input.admission ? { admission: input.admission } : {}),
|
|
267
277
|
}, child.attemptHandle),
|
|
268
278
|
};
|
|
269
279
|
}
|
|
@@ -340,7 +350,8 @@ export async function runNativeChatAttempt(
|
|
|
340
350
|
const physicalBudget = sendBudget ?? createInferenceSendBudget(req, logCtx);
|
|
341
351
|
let activeAdapter: ProviderAdapter = createOpenAIChatAdapter(activeProvider);
|
|
342
352
|
let activeRequest: AdapterRequest;
|
|
343
|
-
const replayScope = nativeChatReplayScope(req.headers
|
|
353
|
+
const replayScope = nativeChatReplayScope(req.headers,
|
|
354
|
+
resolveContextPrincipal(req, config, execution.admission ?? logIds?.admission));
|
|
344
355
|
let retainedRequestBytes = 0;
|
|
345
356
|
const releaseRetainedRequest = () => {
|
|
346
357
|
if (retainedRequestBytes === 0) return;
|
|
@@ -100,6 +100,7 @@ import {
|
|
|
100
100
|
isApiAuthRequired,
|
|
101
101
|
isDataPlaneAdmissionSecret,
|
|
102
102
|
isProxyAdmissionSecret,
|
|
103
|
+
resolveContextPrincipal,
|
|
103
104
|
type RequestPolicyView,
|
|
104
105
|
type DataPlaneAdmission,
|
|
105
106
|
} from "./auth-cors";
|
|
@@ -1117,6 +1118,8 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1117
1118
|
// Routing keeps Claude-only sidecar overrides; admission policy must follow the live owner.
|
|
1118
1119
|
codexAuthPolicy: config,
|
|
1119
1120
|
...(logIds?.admission ? { admission: logIds.admission } : {}),
|
|
1121
|
+
// The translated request never forwards the caller credential; resolve the principal first.
|
|
1122
|
+
replayPrincipal: { clientPrincipalId: resolveContextPrincipal(req, config, logIds?.admission) },
|
|
1120
1123
|
...(logIds?.turnAdmissionLease ? { turnAdmissionLease: logIds.turnAdmissionLease } : {}),
|
|
1121
1124
|
abortSignal: req.signal,
|
|
1122
1125
|
promptCacheKeyIsSharedCohort: cacheKeySource === "system",
|