@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
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { OcxMessagePhase } from "../types";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
4
|
+
import type { SseBlockRewrite } from "./sse-payload-rewrite";
|
|
5
|
+
import { replaceSseDataPayload, sseDataPayload } from "./sse-payload-rewrite";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Opt-in per-provider phase inference for Responses passthrough streams.
|
|
9
|
+
*
|
|
10
|
+
* OpenAI models annotate assistant messages with a commentary/final_answer
|
|
11
|
+
* phase; the Codex desktop UI collapses commentary into progress rows and
|
|
12
|
+
* only fully renders the final answer. Third-party gateways forward plain
|
|
13
|
+
* Responses items without that field, so every narration chunk renders
|
|
14
|
+
* expanded. The wire has no phase field, so phase is inferred from what
|
|
15
|
+
* follows each completed message, mirroring the openai-chat bridge strategy:
|
|
16
|
+
* a message followed by tool-call work is commentary; a message at turn end
|
|
17
|
+
* is the final answer. Messages already carrying commentary or final_answer
|
|
18
|
+
* are left untouched and forwarded without buffering; a null or unknown phase
|
|
19
|
+
* is inferred like a missing one. Each rewritten message stays
|
|
20
|
+
* buffered until the next protocol event (SSE keepalives keep it pending).
|
|
21
|
+
* Live text deltas are not buffered, but message completion waits for that
|
|
22
|
+
* event. Snapshots reuse each item decision when their phase is absent,
|
|
23
|
+
* matched by item id or, when either side lacks an id, by output index;
|
|
24
|
+
* explicit snapshot phases take precedence, and failed snapshots are untouched.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const TERMINAL_EVENT_TYPES = new Set([
|
|
28
|
+
"response.completed",
|
|
29
|
+
"response.incomplete",
|
|
30
|
+
"response.failed",
|
|
31
|
+
]);
|
|
32
|
+
const TOOL_CALL_ITEM_TYPES = new Set([
|
|
33
|
+
"function_call",
|
|
34
|
+
"custom_tool_call",
|
|
35
|
+
"local_shell_call",
|
|
36
|
+
"shell_call",
|
|
37
|
+
"apply_patch_call",
|
|
38
|
+
"mcp_call",
|
|
39
|
+
"tool_search_call",
|
|
40
|
+
"web_search_call",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Only these two values are decisions; null or unknown phases count as missing. */
|
|
48
|
+
function isMessagePhase(value: unknown): value is OcxMessagePhase {
|
|
49
|
+
return value === "commentary" || value === "final_answer";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isToolCallItem(item: unknown): boolean {
|
|
53
|
+
if (!isPlainObject(item)) return false;
|
|
54
|
+
const type = item.type;
|
|
55
|
+
return typeof type === "string"
|
|
56
|
+
&& (TOOL_CALL_ITEM_TYPES.has(type)
|
|
57
|
+
// Unknown hosted/extension call shapes still prove the turn continued.
|
|
58
|
+
|| (type.endsWith("_call") && !type.endsWith("_call_output")));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseDataPayload(block: string): Record<string, unknown> | null {
|
|
62
|
+
const payload = sseDataPayload(block);
|
|
63
|
+
if (payload === null) return null;
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(payload) as unknown;
|
|
66
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isDoneSentinel(block: string): boolean {
|
|
73
|
+
return sseDataPayload(block)?.trim() === "[DONE]";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isMessageDoneBlock(block: string): boolean {
|
|
77
|
+
const parsed = parseDataPayload(block);
|
|
78
|
+
return parsed?.type === "response.output_item.done"
|
|
79
|
+
&& isPlainObject(parsed.item) && parsed.item.type === "message";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function withMessagePhase(block: string, phase: OcxMessagePhase): string {
|
|
83
|
+
const parsed = parseDataPayload(block);
|
|
84
|
+
if (!parsed || parsed.type !== "response.output_item.done") return block;
|
|
85
|
+
if (!isPlainObject(parsed.item) || isMessagePhase(parsed.item.phase)) return block;
|
|
86
|
+
const patched = { ...parsed, item: { ...parsed.item, phase } };
|
|
87
|
+
return replaceSseDataPayload(block, JSON.stringify(patched));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One per-item decision, findable by item id and by output index. */
|
|
91
|
+
interface PhaseDecision {
|
|
92
|
+
readonly id: string | null;
|
|
93
|
+
readonly index: number | null;
|
|
94
|
+
readonly phase: OcxMessagePhase;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface PhaseDecisions {
|
|
98
|
+
readonly byId: ReadonlyMap<string, PhaseDecision>;
|
|
99
|
+
readonly byIndex: ReadonlyMap<number, PhaseDecision>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function outputIndexOf(value: unknown): number | null {
|
|
103
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The phase already emitted on the wire for the snapshot item at `position`.
|
|
108
|
+
* An id match wins. A position match is used only when the event or the
|
|
109
|
+
* snapshot item lacks an id, so two different ids never share a decision.
|
|
110
|
+
*/
|
|
111
|
+
function decidedPhase(decisions: PhaseDecisions, id: string | null, position: number): OcxMessagePhase | undefined {
|
|
112
|
+
const byId = id !== null ? decisions.byId.get(id) : undefined;
|
|
113
|
+
if (byId) return byId.phase;
|
|
114
|
+
const byIndex = decisions.byIndex.get(position);
|
|
115
|
+
return byIndex && (byIndex.id === null || id === null) ? byIndex.phase : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function withSnapshotPhases(block: string, decisions: PhaseDecisions): string {
|
|
119
|
+
const parsed = parseDataPayload(block);
|
|
120
|
+
if (!parsed) return block;
|
|
121
|
+
const response = parsed.response;
|
|
122
|
+
if (!isPlainObject(response) || !Array.isArray(response.output)) return block;
|
|
123
|
+
let changed = false;
|
|
124
|
+
const output = response.output.map((item: unknown, position: number) => {
|
|
125
|
+
if (isPlainObject(item) && item.type === "message" && !isMessagePhase(item.phase)) {
|
|
126
|
+
// Mirror the decision already emitted for this item on the wire, so
|
|
127
|
+
// clients reconciling the final snapshot never re-expand a message
|
|
128
|
+
// whose output_item.done said commentary. Items the rewrite never saw
|
|
129
|
+
// (joined mid-stream, no buffered decision) fall back to final_answer.
|
|
130
|
+
const id = typeof item.id === "string" ? item.id : null;
|
|
131
|
+
const phase = decidedPhase(decisions, id, position) ?? "final_answer";
|
|
132
|
+
changed = true;
|
|
133
|
+
return { ...item, phase };
|
|
134
|
+
}
|
|
135
|
+
return item;
|
|
136
|
+
});
|
|
137
|
+
if (!changed) return block;
|
|
138
|
+
const patched = { ...parsed, response: { ...response, output } };
|
|
139
|
+
return replaceSseDataPayload(block, JSON.stringify(patched));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createResponsesPhaseInferenceBlockRewrite(budget: TranslatorBudget): SseBlockRewrite {
|
|
143
|
+
// At most one message-done block is held at a time; the next block decides
|
|
144
|
+
// its phase and is then processed itself. Malformed blocks pass through
|
|
145
|
+
// without consuming the pending decision. Per-item decisions fill missing
|
|
146
|
+
// snapshot phases. Both pending blocks and decisions share the request
|
|
147
|
+
// retention budget and are released at a terminal event or teardown.
|
|
148
|
+
let pending: string | null = null;
|
|
149
|
+
let pendingBytes = 0;
|
|
150
|
+
let decisionBytes = 0;
|
|
151
|
+
const byId = new Map<string, PhaseDecision>();
|
|
152
|
+
const byIndex = new Map<number, PhaseDecision>();
|
|
153
|
+
const decisions: PhaseDecisions = { byId, byIndex };
|
|
154
|
+
const scope = { kind: "retained_collectors" as const };
|
|
155
|
+
|
|
156
|
+
const clearState = (): void => {
|
|
157
|
+
budget.releaseRetained(pendingBytes + decisionBytes, scope);
|
|
158
|
+
pending = null;
|
|
159
|
+
pendingBytes = 0;
|
|
160
|
+
decisionBytes = 0;
|
|
161
|
+
byId.clear();
|
|
162
|
+
byIndex.clear();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const decisionSize = (decision: PhaseDecision): number =>
|
|
166
|
+
Buffer.byteLength(JSON.stringify([decision.id, decision.index, decision.phase]));
|
|
167
|
+
|
|
168
|
+
const rememberPhase = (item: unknown, outputIndex: unknown, phase: OcxMessagePhase): void => {
|
|
169
|
+
const id = isPlainObject(item) && typeof item.id === "string" ? item.id : null;
|
|
170
|
+
const index = outputIndexOf(outputIndex);
|
|
171
|
+
if (id === null && index === null) return;
|
|
172
|
+
const next: PhaseDecision = { id, index, phase };
|
|
173
|
+
const replaced = new Set<PhaseDecision>();
|
|
174
|
+
const previousById = id !== null ? byId.get(id) : undefined;
|
|
175
|
+
const previousByIndex = index !== null ? byIndex.get(index) : undefined;
|
|
176
|
+
if (previousById) replaced.add(previousById);
|
|
177
|
+
if (previousByIndex) replaced.add(previousByIndex);
|
|
178
|
+
let delta = decisionSize(next);
|
|
179
|
+
for (const previous of replaced) delta -= decisionSize(previous);
|
|
180
|
+
// Charge before mutating, so a budget rejection leaves state consistent.
|
|
181
|
+
if (delta > 0) budget.chargeRetained(delta, scope);
|
|
182
|
+
else if (delta < 0) budget.releaseRetained(-delta, scope);
|
|
183
|
+
for (const previous of replaced) {
|
|
184
|
+
if (previous.id !== null && byId.get(previous.id) === previous) byId.delete(previous.id);
|
|
185
|
+
if (previous.index !== null && byIndex.get(previous.index) === previous) byIndex.delete(previous.index);
|
|
186
|
+
}
|
|
187
|
+
if (id !== null) byId.set(id, next);
|
|
188
|
+
if (index !== null) byIndex.set(index, next);
|
|
189
|
+
decisionBytes += delta;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const releasePending = (phase: OcxMessagePhase): readonly string[] => {
|
|
193
|
+
if (pending === null) return [];
|
|
194
|
+
const block = pending;
|
|
195
|
+
const parsed = parseDataPayload(block);
|
|
196
|
+
rememberPhase(parsed?.item, parsed?.output_index, phase);
|
|
197
|
+
const patched = withMessagePhase(block, phase);
|
|
198
|
+
budget.releaseRetained(pendingBytes, scope);
|
|
199
|
+
pending = null;
|
|
200
|
+
pendingBytes = 0;
|
|
201
|
+
return [patched];
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const rewrite = (block: string): readonly string[] => {
|
|
205
|
+
const parsed = parseDataPayload(block);
|
|
206
|
+
const type = typeof parsed?.type === "string" ? parsed.type : null;
|
|
207
|
+
if (TERMINAL_EVENT_TYPES.has(type!)) {
|
|
208
|
+
// Turn end: any still-buffered message is the final answer, and the
|
|
209
|
+
// terminal snapshot mirrors the same per-item decisions for consistency.
|
|
210
|
+
const released = releasePending("final_answer");
|
|
211
|
+
const snapshot = type === "response.failed" ? block : withSnapshotPhases(block, decisions);
|
|
212
|
+
clearState();
|
|
213
|
+
return [...released, snapshot];
|
|
214
|
+
}
|
|
215
|
+
if (type === "response.output_item.done" && isMessageDoneBlock(block)) {
|
|
216
|
+
const item = parsed?.item;
|
|
217
|
+
if (isPlainObject(item) && isMessagePhase(item.phase)) {
|
|
218
|
+
// An explicit upstream phase needs no inference: forward immediately
|
|
219
|
+
// instead of buffering a message that an abnormal teardown would
|
|
220
|
+
// otherwise drop before it was ever emitted.
|
|
221
|
+
const released = releasePending("commentary");
|
|
222
|
+
rememberPhase(item, parsed?.output_index, item.phase);
|
|
223
|
+
return [...released, block];
|
|
224
|
+
}
|
|
225
|
+
// A following message done means the turn continued past the buffered
|
|
226
|
+
// one (a phase change or a guarded continuation): commentary. Buffer
|
|
227
|
+
// the current block for classification by whatever comes next.
|
|
228
|
+
const released = releasePending("commentary");
|
|
229
|
+
const bytes = Buffer.byteLength(block);
|
|
230
|
+
budget.chargeRetained(bytes, scope);
|
|
231
|
+
pending = block;
|
|
232
|
+
pendingBytes = bytes;
|
|
233
|
+
return released;
|
|
234
|
+
}
|
|
235
|
+
if (type === null) {
|
|
236
|
+
// Comment keepalives and other non-JSON data blocks carry no
|
|
237
|
+
// classification evidence in either direction, so a still-buffered
|
|
238
|
+
// message stays pending across them. The [DONE] sentinel closes the
|
|
239
|
+
// stream cleanly, which proves the buffered message was the turn's
|
|
240
|
+
// final answer.
|
|
241
|
+
if (isDoneSentinel(block)) {
|
|
242
|
+
const released = releasePending("final_answer");
|
|
243
|
+
clearState();
|
|
244
|
+
return [...released, block];
|
|
245
|
+
}
|
|
246
|
+
return [block];
|
|
247
|
+
}
|
|
248
|
+
// Any other protocol event after a completed message proves the turn
|
|
249
|
+
// continued past it (tool calls, reasoning, a new item): commentary.
|
|
250
|
+
return [...releasePending("commentary"), block];
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return Object.assign((block: string): readonly string[] => {
|
|
254
|
+
try { return rewrite(block); }
|
|
255
|
+
catch (error) {
|
|
256
|
+
clearState();
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}, {
|
|
260
|
+
dispose: () => {
|
|
261
|
+
// Relay teardown after a terminal event leaves nothing pending. An
|
|
262
|
+
// abnormal teardown before classification drops the retained block,
|
|
263
|
+
// since the stream is failing anyway and no phase was ever proven.
|
|
264
|
+
clearState();
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
@@ -96,6 +96,8 @@ export function createSseBlockBuffer(
|
|
|
96
96
|
let buffer = "";
|
|
97
97
|
let offset = 0;
|
|
98
98
|
let scanOffset = 0;
|
|
99
|
+
let needsScan = false;
|
|
100
|
+
let lastCodeUnit = Number.NaN;
|
|
99
101
|
let bufferBytes = 0;
|
|
100
102
|
const delimiterPattern = /(?:\r\n|\r(?!\n)|\n){2}/g;
|
|
101
103
|
|
|
@@ -111,7 +113,7 @@ export function createSseBlockBuffer(
|
|
|
111
113
|
if (!fragment) return;
|
|
112
114
|
let fragmentBytes = Buffer.byteLength(fragment, "utf8");
|
|
113
115
|
// Decoder output never splits a surrogate pair, but keep the helper exact for string callers.
|
|
114
|
-
const last =
|
|
116
|
+
const last = lastCodeUnit;
|
|
115
117
|
const first = fragment.charCodeAt(0);
|
|
116
118
|
if (offset < buffer.length && last >= 0xd800 && last <= 0xdbff && first >= 0xdc00 && first <= 0xdfff) {
|
|
117
119
|
fragmentBytes -= 2;
|
|
@@ -122,6 +124,8 @@ export function createSseBlockBuffer(
|
|
|
122
124
|
try {
|
|
123
125
|
compact();
|
|
124
126
|
buffer += fragment;
|
|
127
|
+
lastCodeUnit = fragment.charCodeAt(fragment.length - 1);
|
|
128
|
+
needsScan ||= fragment.includes("\r") || fragment.includes("\n");
|
|
125
129
|
reservation.commitRetained();
|
|
126
130
|
budget.releaseRetained(bufferBytes, scope);
|
|
127
131
|
bufferBytes = nextBytes;
|
|
@@ -131,11 +135,16 @@ export function createSseBlockBuffer(
|
|
|
131
135
|
}
|
|
132
136
|
},
|
|
133
137
|
next() {
|
|
138
|
+
if (!needsScan) {
|
|
139
|
+
scanOffset = Math.max(offset, buffer.length - 3);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
134
142
|
delimiterPattern.lastIndex = scanOffset;
|
|
135
143
|
const match = delimiterPattern.exec(buffer);
|
|
136
144
|
if (!match) {
|
|
137
145
|
// A delimiter is at most four code units. Revisit only its possible split prefix.
|
|
138
146
|
scanOffset = Math.max(offset, buffer.length - 3);
|
|
147
|
+
needsScan = false;
|
|
139
148
|
return null;
|
|
140
149
|
}
|
|
141
150
|
const start = match.index;
|
|
@@ -153,6 +162,8 @@ export function createSseBlockBuffer(
|
|
|
153
162
|
buffer = "";
|
|
154
163
|
offset = 0;
|
|
155
164
|
scanOffset = 0;
|
|
165
|
+
needsScan = false;
|
|
166
|
+
lastCodeUnit = Number.NaN;
|
|
156
167
|
}
|
|
157
168
|
return { block, delimiter };
|
|
158
169
|
},
|
|
@@ -164,6 +175,8 @@ export function createSseBlockBuffer(
|
|
|
164
175
|
buffer = "";
|
|
165
176
|
offset = 0;
|
|
166
177
|
scanOffset = 0;
|
|
178
|
+
needsScan = false;
|
|
179
|
+
lastCodeUnit = Number.NaN;
|
|
167
180
|
bufferBytes = 0;
|
|
168
181
|
},
|
|
169
182
|
};
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* route back through them (see `installedServiceListenPort` in `health.ts`, which
|
|
8
8
|
* intentionally stays behind because it asks every backend).
|
|
9
9
|
*/
|
|
10
|
-
import { loadConfig } from "../config";
|
|
10
|
+
import { getConfigDir, loadConfig } from "../config";
|
|
11
|
+
import { readClaudeInterceptProxyToken } from "../claude/intercept/proxy-auth";
|
|
11
12
|
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
12
13
|
import { PROXY_ENV_KEYS } from "../lib/proxy-env";
|
|
13
14
|
|
|
@@ -87,19 +88,30 @@ export function parseBakedListenPort(read: () => string): number | null {
|
|
|
87
88
|
* own `applyProxyEnv` already treats both cases as equivalent. Only the canonical
|
|
88
89
|
* upper-case name is baked, so a definition never carries two spellings of one setting.
|
|
89
90
|
*/
|
|
90
|
-
export function resolvedProxyEnv(
|
|
91
|
+
export function resolvedProxyEnv(
|
|
92
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
93
|
+
ownClaudeInterceptProxy?: string | null,
|
|
94
|
+
): { name: string; value: string }[] {
|
|
95
|
+
const token = ownClaudeInterceptProxy === undefined ? readClaudeInterceptProxyToken(getConfigDir()) : null;
|
|
91
96
|
const resolved: { name: string; value: string }[] = [];
|
|
92
97
|
for (const key of PROXY_ENV_KEYS) {
|
|
93
|
-
const value = [env[key]?.trim(), env[key.toLowerCase()]?.trim()].find(v => v &&
|
|
98
|
+
const value = [env[key]?.trim(), env[key.toLowerCase()]?.trim()].find(v => v && (
|
|
99
|
+
ownClaudeInterceptProxy === undefined ? !isOwnedClaudeInterceptProxy(v, token) : v !== ownClaudeInterceptProxy
|
|
100
|
+
));
|
|
94
101
|
if (value) resolved.push({ name: key, value });
|
|
95
102
|
}
|
|
96
103
|
return resolved;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* is matched: a plain `http://127.0.0.1:<port>` is a user's local proxy client and is kept.
|
|
107
|
+
* Match this installation's credential and exact URL components without creating a token.
|
|
108
|
+
* An inherited URL can still use an old bound port after the persisted config changes.
|
|
109
|
+
* Shape alone cannot establish ownership: a user proxy may also use an `opencodex` username.
|
|
104
110
|
*/
|
|
105
|
-
|
|
111
|
+
function isOwnedClaudeInterceptProxy(value: string, token: string | null): boolean {
|
|
112
|
+
if (!token) return false;
|
|
113
|
+
const prefix = `http://opencodex:${encodeURIComponent(token)}@127.0.0.1:`;
|
|
114
|
+
if (!value.startsWith(prefix)) return false;
|
|
115
|
+
const port = value.slice(prefix.length).replace(/\/$/, "");
|
|
116
|
+
return /^\d{1,5}$/.test(port) && Number(port) > 0 && Number(port) <= 65535;
|
|
117
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -1496,6 +1496,12 @@ export interface OcxComboConfig {
|
|
|
1496
1496
|
decisionQuotaSignals?: boolean | null;
|
|
1497
1497
|
/** Partial 0..100 tier overrides; defaults limited 70/nearlyExhausted 90. null clears. */
|
|
1498
1498
|
decisionQuotaTiers?: import("../combos/jev-quota-config").JevQuotaTiers | null;
|
|
1499
|
+
/**
|
|
1500
|
+
* `strategy: "jev"` only, default off: keep an explicit client session on the target JEV chose
|
|
1501
|
+
* for it while the session stays active, so the provider prompt cache stays warm (#7525).
|
|
1502
|
+
* `ttlMs` is the idle window after the last successful turn (60000..86400000). null clears.
|
|
1503
|
+
*/
|
|
1504
|
+
sessionPin?: import("../combos/jev-session-pin-config").JevSessionPinConfig | null;
|
|
1499
1505
|
}
|
|
1500
1506
|
|
|
1501
1507
|
export type OcxRoutingUnknownEvidenceMode = "allow" | "penalize" | "exclude";
|
package/src/types/provider.ts
CHANGED
|
@@ -871,6 +871,14 @@ export interface OcxProviderConfig {
|
|
|
871
871
|
* SSE/JSON; raw inspection state remains authoritative.
|
|
872
872
|
*/
|
|
873
873
|
responsesSnapshotRepair?: boolean;
|
|
874
|
+
/**
|
|
875
|
+
* Infer the commentary/final_answer phase on assistant messages for Responses passthrough
|
|
876
|
+
* providers whose upstream omits it (e.g. third-party gateways). Codex desktop collapses
|
|
877
|
+
* commentary into progress rows; without this every narration renders fully expanded.
|
|
878
|
+
* Disabled by default. Applies in the client-facing SSE rewrite chain, including
|
|
879
|
+
* buffered JSON reconstructed from upstream SSE; ordinary upstream JSON is unchanged.
|
|
880
|
+
*/
|
|
881
|
+
messagePhaseInference?: boolean;
|
|
874
882
|
/**
|
|
875
883
|
* Opt-in hosted-web-search bridge for this KEY-auth Responses passthrough provider (#3761).
|
|
876
884
|
* Absent or disabled leaves the passthrough byte-identical to today.
|
|
@@ -1116,7 +1124,7 @@ export interface OcxProviderConfig {
|
|
|
1116
1124
|
googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
|
|
1117
1125
|
/** Google tool-schema compatibility policy. Omitted preserves compatible report-only behavior. */
|
|
1118
1126
|
googleToolSchemaPolicy?: "compatible" | "reject-lossy";
|
|
1119
|
-
/** Vertex AI GCP project id. API keys use it to select the project endpoint; ADC falls back to GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT env. */
|
|
1127
|
+
/** Vertex AI GCP project id. API keys use it to select the project endpoint; "auto" resolves each API key's own project. ADC falls back to GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT env. */
|
|
1120
1128
|
project?: string;
|
|
1121
1129
|
/** Vertex AI location, e.g. "us-central1" or "global". API-key project endpoints default to "global"; ADC falls back to GOOGLE_CLOUD_LOCATION env. */
|
|
1122
1130
|
location?: string;
|
package/src/types/request.ts
CHANGED
|
@@ -148,6 +148,7 @@ export interface OcxParsedRequest {
|
|
|
148
148
|
_kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion" | "authType">;
|
|
149
149
|
/** Account-scoped Zed user identity paired with the long-lived access token. */
|
|
150
150
|
_zedAuthContext?: { userId: string };
|
|
151
|
+
_workbuddyAuthContext?: import("../oauth/types").WorkBuddyOAuthMetadata;
|
|
151
152
|
/** Provider-private continuation metadata resolved from the Responses previous_response_id chain. */
|
|
152
153
|
_providerContinuation?: OcxProviderContinuationState;
|
|
153
154
|
/** Persisted continuation considered only after the final physical route is known. */
|
package/src/types/wire.ts
CHANGED
|
@@ -101,6 +101,7 @@ function normalizedEndpoint(value: unknown): string | undefined {
|
|
|
101
101
|
* URL. The empty prefix covers every model id.
|
|
102
102
|
*/
|
|
103
103
|
const CREDENTIAL_PINNED_ADAPTERS: Readonly<Record<string, Readonly<Record<string, string>>>> = Object.freeze({
|
|
104
|
+
"workbuddy-ai": Object.freeze({ "": "workbuddy-ai" }),
|
|
104
105
|
"dsh-account": Object.freeze({ "": "dsh-account" }),
|
|
105
106
|
});
|
|
106
107
|
|
|
@@ -16,3 +16,12 @@ export const CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS: readonly [
|
|
|
16
16
|
"VOLTA_HOME",
|
|
17
17
|
];
|
|
18
18
|
export function isCodexCliUpdateInspectionArgv(argv: readonly string[]): boolean;
|
|
19
|
+
export const NODE_LAUNCH_CONTEXT_MAX_CHARS: number;
|
|
20
|
+
export function codexCliInspectionLookupSeeds(
|
|
21
|
+
platform: string,
|
|
22
|
+
home: string | null,
|
|
23
|
+
nodePath: string | null,
|
|
24
|
+
serializedContextChars: number,
|
|
25
|
+
): { home: string | null; nodePath: string | null };
|
|
26
|
+
export const CODEX_CLI_TRANSPORT_ENV_KEYS: readonly string[];
|
|
27
|
+
export function captureCodexCliTransportEnv(env: Readonly<NodeJS.ProcessEnv>): Record<string, string>;
|
|
@@ -16,6 +16,16 @@ export const CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS = Object.freeze([
|
|
|
16
16
|
"VOLTA_HOME",
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
|
+
export const CODEX_CLI_TRANSPORT_ENV_KEYS = Object.freeze([
|
|
20
|
+
"http_proxy", "https_proxy", "all_proxy", "no_proxy", "node_extra_ca_certs",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/** Preserve spelling, aliases and empty values before Bun loads project dotenv. */
|
|
24
|
+
export function captureCodexCliTransportEnv(env) {
|
|
25
|
+
return Object.fromEntries(Object.entries(env).filter(([name, value]) =>
|
|
26
|
+
CODEX_CLI_TRANSPORT_ENV_KEYS.includes(name.toLowerCase()) && typeof value === "string"));
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
/**
|
|
20
30
|
* Detect the read-only Codex CLI updater inspection namespace before Bun loads.
|
|
21
31
|
* Keep this exact and argument-position based: malformed actions still inherit
|
|
@@ -28,3 +38,23 @@ export function isCodexCliUpdateInspectionArgv(argv) {
|
|
|
28
38
|
const args = argv.slice(2).filter(value => !value.startsWith("--ocx-internal-launch-proof="));
|
|
29
39
|
return args[0] === "system" && args[1] === "codex-cli-update";
|
|
30
40
|
}
|
|
41
|
+
|
|
42
|
+
/** Serialized OCX_NODE_LAUNCH_CONTEXT ceiling; src/cli/launcher-context.ts refuses above it. */
|
|
43
|
+
export const NODE_LAUNCH_CONTEXT_MAX_CHARS = 64 * 1024;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The HOME/NODE_PATH seeds the update inspection's modeled Node lookup needs,
|
|
47
|
+
* bounded against the launch-context budget. They are POSIX-only: a Windows
|
|
48
|
+
* inspection never models the global lookup, so the seeds are not sent at all
|
|
49
|
+
* (and their env copies stay inherited for the child's own use). If carrying
|
|
50
|
+
* them would push the serialized context past the reader's limit they are
|
|
51
|
+
* dropped instead of taking the whole snapshot down with them — absent seeds
|
|
52
|
+
* leave only the global lane unverifiable, which fails the content
|
|
53
|
+
* observation closed while the selection context survives.
|
|
54
|
+
*/
|
|
55
|
+
export function codexCliInspectionLookupSeeds(platform, home, nodePath, serializedContextChars) {
|
|
56
|
+
if (platform === "win32" || serializedContextChars > NODE_LAUNCH_CONTEXT_MAX_CHARS) {
|
|
57
|
+
return { home: null, nodePath: null };
|
|
58
|
+
}
|
|
59
|
+
return { home: home ?? null, nodePath: nodePath ?? null };
|
|
60
|
+
}
|
package/src/usage/jev-stats.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeJevQuotaSummary, type JevQuotaDecisionSummary } from "../combos/jev-quota-config";
|
|
2
|
+
import { JEV_SESSION_PIN_REASONS, type JevSessionPinReason } from "../combos/jev-session-pin-config";
|
|
2
3
|
import type { OcxComboDefaultEffort } from "../types";
|
|
3
4
|
import type { PersistedUsageEntry } from "./log";
|
|
4
5
|
import { usageDisplayTotalTokens } from "./totals";
|
|
@@ -48,6 +49,10 @@ export interface PersistedJevDecisionV1 {
|
|
|
48
49
|
outputTokens: number;
|
|
49
50
|
totalTokens: number;
|
|
50
51
|
};
|
|
52
|
+
/** The turn reused its session pin (#7525); no decision request was sent for it. */
|
|
53
|
+
pinned?: true;
|
|
54
|
+
/** `session` with `pinned`; otherwise why an existing session pin was released. */
|
|
55
|
+
pinReason?: JevSessionPinReason;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
export interface JevStatsModelRow {
|
|
@@ -88,6 +93,8 @@ export interface JevStatsResponse {
|
|
|
88
93
|
decisions: number;
|
|
89
94
|
appliedDecisions: number;
|
|
90
95
|
failOpenDecisions: number;
|
|
96
|
+
/** Requests that reused a session pin; they are not decisions and add only physical attempts. */
|
|
97
|
+
pinnedRequests: number;
|
|
91
98
|
successfulRequests: number;
|
|
92
99
|
requestsWithModelFallback: number;
|
|
93
100
|
modelAttempts: number;
|
|
@@ -188,6 +195,13 @@ export function normalizePersistedJevDecision(value: unknown): PersistedJevDecis
|
|
|
188
195
|
const backend = value.backend === "typesafe" || value.backend === "systemone" || value.backend === "model"
|
|
189
196
|
? value.backend
|
|
190
197
|
: undefined;
|
|
198
|
+
const pinReason = typeof value.pinReason === "string"
|
|
199
|
+
&& (JEV_SESSION_PIN_REASONS as readonly string[]).includes(value.pinReason)
|
|
200
|
+
? value.pinReason as JevSessionPinReason
|
|
201
|
+
: undefined;
|
|
202
|
+
// A pinned turn always says so with `session`; any other pairing is not a record this writes.
|
|
203
|
+
const pinned = value.pinned === true && pinReason === "session";
|
|
204
|
+
if ((value.pinned === true || pinReason === "session") && !pinned) return undefined;
|
|
191
205
|
return {
|
|
192
206
|
version: 1,
|
|
193
207
|
comboId,
|
|
@@ -203,6 +217,8 @@ export function normalizePersistedJevDecision(value: unknown): PersistedJevDecis
|
|
|
203
217
|
...(confidence !== undefined ? { confidence } : {}),
|
|
204
218
|
...(chosenProbability !== undefined ? { chosenProbability } : {}),
|
|
205
219
|
...(usage ? { usage } : {}),
|
|
220
|
+
...(pinned ? { pinned: true as const } : {}),
|
|
221
|
+
...(pinReason ? { pinReason } : {}),
|
|
206
222
|
};
|
|
207
223
|
}
|
|
208
224
|
|
|
@@ -210,6 +226,24 @@ interface MutableModelRow extends Omit<JevStatsModelRow, "efforts"> {
|
|
|
210
226
|
effortCounts: Map<OcxComboDefaultEffort | null, number>;
|
|
211
227
|
}
|
|
212
228
|
|
|
229
|
+
interface SentAttempt {
|
|
230
|
+
attempt: NonNullable<PersistedUsageEntry["attempts"]>[number];
|
|
231
|
+
provider: string;
|
|
232
|
+
model: string;
|
|
233
|
+
sendCount: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Attempts that physically sent, with bounded identities; zero-send rows never count. */
|
|
237
|
+
function sentAttempts(entry: PersistedUsageEntry): SentAttempt[] {
|
|
238
|
+
return (entry.attempts ?? []).flatMap(attempt => {
|
|
239
|
+
const sendCount = boundedCount(attempt.sendCount) ?? 0;
|
|
240
|
+
if (sendCount === 0) return [];
|
|
241
|
+
const provider = boundedIdentity(attempt.provider, MAX_TARGET_IDENTITY_CHARS);
|
|
242
|
+
const model = boundedIdentity(attempt.model, MAX_TARGET_IDENTITY_CHARS);
|
|
243
|
+
return provider && model ? [{ attempt, provider, model, sendCount }] : [];
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
213
247
|
interface JevStatsAccumulatorOptions {
|
|
214
248
|
comboId?: string | null;
|
|
215
249
|
since?: number | null;
|
|
@@ -291,6 +325,7 @@ class StreamingJevStatsAccumulator implements JevStatsAccumulator {
|
|
|
291
325
|
private snapshotWindowStart: number | null = null;
|
|
292
326
|
private snapshotWindowEnd: number | null = null;
|
|
293
327
|
private decisions = 0;
|
|
328
|
+
private pinnedRequests = 0;
|
|
294
329
|
private appliedDecisions = 0;
|
|
295
330
|
private successfulRequests = 0;
|
|
296
331
|
private requestsWithModelFallback = 0;
|
|
@@ -358,6 +393,21 @@ class StreamingJevStatsAccumulator implements JevStatsAccumulator {
|
|
|
358
393
|
|| (this.until !== undefined && entry.timestamp > this.until)) return;
|
|
359
394
|
const decision = normalizePersistedJevDecision(entry.jevDecision);
|
|
360
395
|
if (!decision || (this.comboId !== null && decision.comboId !== this.comboId)) return;
|
|
396
|
+
const attempts = sentAttempts(entry);
|
|
397
|
+
// Request-level outcomes count every JEV-routed request, pinned or decided.
|
|
398
|
+
if (entry.status >= 200 && entry.status < 400) {
|
|
399
|
+
this.successfulRequests = saturatingAdd(this.successfulRequests, 1);
|
|
400
|
+
}
|
|
401
|
+
if (attempts.some(({ provider, model }) => provider !== decision.selected.provider
|
|
402
|
+
|| model !== decision.selected.model)) {
|
|
403
|
+
this.requestsWithModelFallback = saturatingAdd(this.requestsWithModelFallback, 1);
|
|
404
|
+
}
|
|
405
|
+
this.addAttempts(attempts);
|
|
406
|
+
if (decision.pinned) {
|
|
407
|
+
// A session-pinned turn sent no decision request, so it stays out of every decision total.
|
|
408
|
+
this.pinnedRequests = saturatingAdd(this.pinnedRequests, 1);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
361
411
|
|
|
362
412
|
const backend = decision.backend ?? "unknown";
|
|
363
413
|
const backendRow = this.backends.get(backend)
|
|
@@ -369,9 +419,6 @@ class StreamingJevStatsAccumulator implements JevStatsAccumulator {
|
|
|
369
419
|
this.averageLatencyMs = nextMean(this.averageLatencyMs, this.decisions, decision.latencyMs);
|
|
370
420
|
this.decisions = saturatingAdd(this.decisions, 1);
|
|
371
421
|
if (decision.gate === "apply") this.appliedDecisions = saturatingAdd(this.appliedDecisions, 1);
|
|
372
|
-
if (entry.status >= 200 && entry.status < 400) {
|
|
373
|
-
this.successfulRequests = saturatingAdd(this.successfulRequests, 1);
|
|
374
|
-
}
|
|
375
422
|
this.gates.set(decision.gate, saturatingAdd(this.gates.get(decision.gate) ?? 0, 1));
|
|
376
423
|
if (decision.confidence !== undefined) {
|
|
377
424
|
this.confidenceTotal = saturatingAdd(this.confidenceTotal, decision.confidence);
|
|
@@ -396,18 +443,9 @@ class StreamingJevStatsAccumulator implements JevStatsAccumulator {
|
|
|
396
443
|
decision.selected.effort,
|
|
397
444
|
saturatingAdd(selected.effortCounts.get(decision.selected.effort) ?? 0, 1),
|
|
398
445
|
);
|
|
446
|
+
}
|
|
399
447
|
|
|
400
|
-
|
|
401
|
-
const sendCount = boundedCount(attempt.sendCount) ?? 0;
|
|
402
|
-
if (sendCount === 0) return [];
|
|
403
|
-
const provider = boundedIdentity(attempt.provider, MAX_TARGET_IDENTITY_CHARS);
|
|
404
|
-
const model = boundedIdentity(attempt.model, MAX_TARGET_IDENTITY_CHARS);
|
|
405
|
-
return provider && model ? [{ attempt, provider, model, sendCount }] : [];
|
|
406
|
-
});
|
|
407
|
-
if (attempts.some(({ provider, model }) => provider !== decision.selected.provider
|
|
408
|
-
|| model !== decision.selected.model)) {
|
|
409
|
-
this.requestsWithModelFallback = saturatingAdd(this.requestsWithModelFallback, 1);
|
|
410
|
-
}
|
|
448
|
+
private addAttempts(attempts: readonly SentAttempt[]): void {
|
|
411
449
|
for (const { attempt, provider, model: modelId, sendCount } of attempts) {
|
|
412
450
|
const model = this.rowFor(provider, modelId);
|
|
413
451
|
model.attempts = saturatingAdd(model.attempts, sendCount);
|
|
@@ -449,6 +487,7 @@ class StreamingJevStatsAccumulator implements JevStatsAccumulator {
|
|
|
449
487
|
cloned.snapshotWindowStart = this.snapshotWindowStart;
|
|
450
488
|
cloned.snapshotWindowEnd = this.snapshotWindowEnd;
|
|
451
489
|
cloned.decisions = this.decisions;
|
|
490
|
+
cloned.pinnedRequests = this.pinnedRequests;
|
|
452
491
|
cloned.appliedDecisions = this.appliedDecisions;
|
|
453
492
|
cloned.successfulRequests = this.successfulRequests;
|
|
454
493
|
cloned.requestsWithModelFallback = this.requestsWithModelFallback;
|
|
@@ -492,6 +531,7 @@ class StreamingJevStatsAccumulator implements JevStatsAccumulator {
|
|
|
492
531
|
decisions: this.decisions,
|
|
493
532
|
appliedDecisions: this.appliedDecisions,
|
|
494
533
|
failOpenDecisions: this.decisions - this.appliedDecisions,
|
|
534
|
+
pinnedRequests: this.pinnedRequests,
|
|
495
535
|
successfulRequests: this.successfulRequests,
|
|
496
536
|
requestsWithModelFallback: this.requestsWithModelFallback,
|
|
497
537
|
modelAttempts: this.modelAttempts,
|