@centerforagenticai/pi-multi-account 0.1.1 → 0.1.4
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 +64 -962
- package/package.json +1 -1
- package/src/anthropic-adaptive-stream.ts +81 -15
- package/src/anthropic-alias-stream.ts +9 -1
- package/src/codex-adapter.ts +28 -4
- package/src/error-classification.ts +13 -1
- package/src/host-final-stop-message.ts +124 -0
- package/src/index.ts +45 -68
- package/src/logical-provider.ts +30 -1
- package/src/openai-adapter.ts +18 -8
- package/src/pi-ai-entrypoints.ts +138 -0
- package/src/provider-registration.ts +155 -7
- package/src/routing.ts +17 -1
- package/src/shared-usage.ts +83 -176
- package/src/usage-fetch.ts +59 -22
- package/src/usage.ts +29 -26
package/package.json
CHANGED
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
* Provenance: pi-anthropic-oauth@0.2.4-intel.2, private pin
|
|
25
25
|
* 99ac00f290efbcb93e7f23e6b7482d8727367aa7, upstream baseline
|
|
26
26
|
* 53266ecb51b6d1890ef3f7251a64cb1d71d96099, source src/stream.ts.
|
|
27
|
-
* Local delta: adaptive thinking emits type=adaptive and mapped output effort
|
|
28
|
-
* without budget_tokens;
|
|
27
|
+
* Local delta: adaptive thinking emits type=adaptive and mapped output effort
|
|
28
|
+
* without budget_tokens; request-local retry/timeout options reach the SDK; and
|
|
29
|
+
* refusal or unknown stop reasons retain bounded, structured error details.
|
|
29
30
|
*/
|
|
30
31
|
|
|
31
32
|
import { Anthropic } from "@anthropic-ai/sdk";
|
|
@@ -41,6 +42,8 @@ import {
|
|
|
41
42
|
type SimpleStreamOptions,
|
|
42
43
|
type StopReason,
|
|
43
44
|
} from "@earendil-works/pi-ai";
|
|
45
|
+
import { sanitizeDiagnosticText } from "./diagnostics.js";
|
|
46
|
+
import type { ProviderErrorCode } from "./error-classification.js";
|
|
44
47
|
type IndexedBlock =
|
|
45
48
|
| ({ type: "text"; text: string } & { index: number })
|
|
46
49
|
| ({ type: "thinking"; thinking: string; thinkingSignature?: string } & {
|
|
@@ -95,18 +98,51 @@ const REQUIRED_BETAS = [
|
|
|
95
98
|
"interleaved-thinking-2025-05-14",
|
|
96
99
|
] as const;
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
type StopReasonResult = Readonly<{
|
|
102
|
+
stopReason: StopReason;
|
|
103
|
+
errorMessage?: string;
|
|
104
|
+
code?: Extract<ProviderErrorCode, "refusal" | "unknown_stop">;
|
|
105
|
+
}>;
|
|
106
|
+
|
|
107
|
+
function mapStopReason(
|
|
108
|
+
reason: string | null | undefined,
|
|
109
|
+
stopDetails?: unknown,
|
|
110
|
+
): StopReasonResult {
|
|
99
111
|
switch (reason) {
|
|
100
112
|
case "end_turn":
|
|
101
113
|
case "pause_turn":
|
|
102
114
|
case "stop_sequence":
|
|
103
|
-
return "stop";
|
|
115
|
+
return { stopReason: "stop" };
|
|
104
116
|
case "max_tokens":
|
|
105
|
-
return "length";
|
|
117
|
+
return { stopReason: "length" };
|
|
106
118
|
case "tool_use":
|
|
107
|
-
return "toolUse";
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
return { stopReason: "toolUse" };
|
|
120
|
+
case "refusal": {
|
|
121
|
+
const explanation =
|
|
122
|
+
typeof stopDetails === "object" &&
|
|
123
|
+
stopDetails !== null &&
|
|
124
|
+
typeof (stopDetails as { explanation?: unknown }).explanation === "string"
|
|
125
|
+
? sanitizeDiagnosticText(
|
|
126
|
+
(stopDetails as { explanation: string }).explanation,
|
|
127
|
+
)
|
|
128
|
+
: "";
|
|
129
|
+
return {
|
|
130
|
+
stopReason: "error",
|
|
131
|
+
errorMessage:
|
|
132
|
+
explanation || "The model refused to complete the request",
|
|
133
|
+
code: "refusal",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
default: {
|
|
137
|
+
const boundedReason = sanitizeDiagnosticText(reason ?? "unknown");
|
|
138
|
+
return {
|
|
139
|
+
stopReason: "error",
|
|
140
|
+
errorMessage: sanitizeDiagnosticText(
|
|
141
|
+
`Provider stopped with: ${boundedReason}`,
|
|
142
|
+
),
|
|
143
|
+
code: "unknown_stop",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
110
146
|
}
|
|
111
147
|
}
|
|
112
148
|
|
|
@@ -163,7 +199,7 @@ export function streamAnthropicAdaptive(
|
|
|
163
199
|
const stream = createAssistantMessageEventStream();
|
|
164
200
|
|
|
165
201
|
void (async () => {
|
|
166
|
-
const output: AssistantMessage = {
|
|
202
|
+
const output: AssistantMessage & { code?: ProviderErrorCode } = {
|
|
167
203
|
role: "assistant",
|
|
168
204
|
content: [],
|
|
169
205
|
api: model.api,
|
|
@@ -276,10 +312,24 @@ export function streamAnthropicAdaptive(
|
|
|
276
312
|
// which throws under fine-grained-tool-streaming (input may be invalid
|
|
277
313
|
// mid-flight) and aborts the turn. The raw stream yields the same
|
|
278
314
|
// RawMessageStreamEvents; tool args are already parsed leniently below.
|
|
315
|
+
const maxRetries = options?.maxRetries;
|
|
316
|
+
const timeoutMs = options?.timeoutMs;
|
|
279
317
|
const { data: anthropicStream, response: httpResponse } =
|
|
280
318
|
await client.messages
|
|
281
319
|
.create(params, {
|
|
282
320
|
signal: options?.signal,
|
|
321
|
+
...(Number.isFinite(maxRetries) &&
|
|
322
|
+
Number.isInteger(maxRetries) &&
|
|
323
|
+
(maxRetries ?? -1) >= 0
|
|
324
|
+
? { maxRetries }
|
|
325
|
+
: {}),
|
|
326
|
+
// Any finite positive timeout is honored; the SDK timer takes whole
|
|
327
|
+
// milliseconds, so a fractional value is floored to at least 1.
|
|
328
|
+
...(typeof timeoutMs === "number" &&
|
|
329
|
+
Number.isFinite(timeoutMs) &&
|
|
330
|
+
timeoutMs > 0
|
|
331
|
+
? { timeout: Math.max(1, Math.floor(timeoutMs)) }
|
|
332
|
+
: {}),
|
|
283
333
|
})
|
|
284
334
|
.withResponse();
|
|
285
335
|
|
|
@@ -474,7 +524,19 @@ export function streamAnthropicAdaptive(
|
|
|
474
524
|
}
|
|
475
525
|
|
|
476
526
|
if (event.type === "message_delta") {
|
|
477
|
-
|
|
527
|
+
const rawStopReason = event.delta.stop_reason;
|
|
528
|
+
const mapped = mapStopReason(
|
|
529
|
+
rawStopReason,
|
|
530
|
+
(event.delta as { stop_details?: unknown }).stop_details,
|
|
531
|
+
);
|
|
532
|
+
output.stopReason = mapped.stopReason;
|
|
533
|
+
if (typeof rawStopReason === "string") {
|
|
534
|
+
output.rawStopReason = sanitizeDiagnosticText(rawStopReason);
|
|
535
|
+
}
|
|
536
|
+
if (mapped.errorMessage !== undefined) {
|
|
537
|
+
output.errorMessage = mapped.errorMessage;
|
|
538
|
+
}
|
|
539
|
+
if (mapped.code !== undefined) output.code = mapped.code;
|
|
478
540
|
output.usage.input =
|
|
479
541
|
(event.usage as { input_tokens?: number }).input_tokens ||
|
|
480
542
|
output.usage.input;
|
|
@@ -505,11 +567,15 @@ export function streamAnthropicAdaptive(
|
|
|
505
567
|
}
|
|
506
568
|
|
|
507
569
|
if (options?.signal?.aborted) throw new Error("Request aborted");
|
|
508
|
-
|
|
509
|
-
type: "
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
570
|
+
if (output.stopReason === "error") {
|
|
571
|
+
stream.push({ type: "error", reason: "error", error: output });
|
|
572
|
+
} else {
|
|
573
|
+
stream.push({
|
|
574
|
+
type: "done",
|
|
575
|
+
reason: output.stopReason as "stop" | "length" | "toolUse",
|
|
576
|
+
message: output,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
513
579
|
stream.end();
|
|
514
580
|
} catch (error) {
|
|
515
581
|
for (const block of output.content as Array<{
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
sanitizeDiagnosticText,
|
|
14
14
|
sanitizeHeaderValue,
|
|
15
15
|
} from "./diagnostics.js";
|
|
16
|
+
import { hostFinalStopMessage } from "./host-final-stop-message.js";
|
|
16
17
|
|
|
17
18
|
export const ANTHROPIC_ALIAS_API = "hypha-anthropic-oauth" as const;
|
|
18
19
|
|
|
@@ -53,12 +54,19 @@ function withAliasAttribution(
|
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Bounds an upstream error and, for a structured refusal or unknown stop,
|
|
59
|
+
* publishes the shared host-final message. A direct alias turn reaches the
|
|
60
|
+
* host's retry and compaction predicates without the unified provider, so
|
|
61
|
+
* provider-authored stop wording must not make the host resend the request.
|
|
62
|
+
*/
|
|
56
63
|
function sanitizeUpstreamError(message: AssistantMessage): AssistantMessage {
|
|
57
64
|
if (message.errorMessage === undefined) return message;
|
|
58
|
-
|
|
65
|
+
const sanitized: AssistantMessage = {
|
|
59
66
|
...message,
|
|
60
67
|
errorMessage: sanitizeDiagnosticText(message.errorMessage),
|
|
61
68
|
};
|
|
69
|
+
return { ...sanitized, ...hostFinalStopMessage(sanitized) };
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
function withAliasEvent(
|
package/src/codex-adapter.ts
CHANGED
|
@@ -235,9 +235,33 @@ function normalizeAliasContext(
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
/**
|
|
238
|
-
*
|
|
238
|
+
* Temporary containment for the Pi 0.99 Codex WebSocket failure: a WebSocket
|
|
239
|
+
* error leaves the session in a state where the next Codex turn crashes with
|
|
240
|
+
* "Cannot read properties of undefined (reading 'length')". Routes this
|
|
241
|
+
* extension owns always request SSE. The base `openai-codex` provider is never
|
|
242
|
+
* touched; only options on calls we already route are adjusted. Remove once the
|
|
243
|
+
* WebSocket path is fixed upstream.
|
|
244
|
+
*/
|
|
245
|
+
export const CODEX_FORCED_TRANSPORT = "sse" as const;
|
|
246
|
+
|
|
247
|
+
/** Returns options with Codex transport pinned to SSE, and whether a value was overridden. */
|
|
248
|
+
export function forceCodexSseOptions<T extends SimpleStreamOptions>(
|
|
249
|
+
options: T | undefined,
|
|
250
|
+
): { options: T; overridden: boolean } {
|
|
251
|
+
if (options?.transport === CODEX_FORCED_TRANSPORT) {
|
|
252
|
+
return { options, overridden: false };
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
options: { ...(options ?? {}), transport: CODEX_FORCED_TRANSPORT } as T,
|
|
256
|
+
overridden: true,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Wraps the exact maintained stream without replacing OAuth. Pi's
|
|
239
262
|
* alias-resolved apiKey and every other option field are forwarded unchanged;
|
|
240
|
-
*
|
|
263
|
+
* model/context identity and callback attribution are adapted, and transport
|
|
264
|
+
* is pinned to SSE (see {@link CODEX_FORCED_TRANSPORT}).
|
|
241
265
|
*/
|
|
242
266
|
export function createCodexAliasStream(
|
|
243
267
|
maintainedStream: NonNullable<ProviderConfig["streamSimple"]>,
|
|
@@ -251,12 +275,12 @@ export function createCodexAliasStream(
|
|
|
251
275
|
};
|
|
252
276
|
const upstreamContext = normalizeAliasContext(context, aliasModel);
|
|
253
277
|
|
|
254
|
-
let upstreamOptions = options;
|
|
278
|
+
let upstreamOptions = forceCodexSseOptions(options).options;
|
|
255
279
|
if (options?.onPayload || options?.onResponse) {
|
|
256
280
|
const aliasOnPayload = options.onPayload;
|
|
257
281
|
const aliasOnResponse = options.onResponse;
|
|
258
282
|
upstreamOptions = {
|
|
259
|
-
...
|
|
283
|
+
...upstreamOptions,
|
|
260
284
|
...(aliasOnPayload
|
|
261
285
|
? {
|
|
262
286
|
onPayload: (payload: unknown) =>
|
|
@@ -13,6 +13,8 @@ export const PROVIDER_ERROR_CODES = [
|
|
|
13
13
|
"model_not_found",
|
|
14
14
|
"unsupported_api_version",
|
|
15
15
|
"invalid_request",
|
|
16
|
+
"refusal",
|
|
17
|
+
"unknown_stop",
|
|
16
18
|
] as const;
|
|
17
19
|
export type ProviderErrorCode = (typeof PROVIDER_ERROR_CODES)[number];
|
|
18
20
|
|
|
@@ -50,10 +52,12 @@ export type FailureCategory =
|
|
|
50
52
|
|
|
51
53
|
export interface FailureClassification {
|
|
52
54
|
readonly category: FailureCategory;
|
|
55
|
+
readonly kind?: "refusal" | "unknown-stop";
|
|
53
56
|
readonly accountAction:
|
|
54
57
|
| "cooldown-and-route"
|
|
55
58
|
| "invalidate-and-route"
|
|
56
|
-
| "route-without-retry"
|
|
59
|
+
| "route-without-retry"
|
|
60
|
+
| "retain-account";
|
|
57
61
|
readonly cooldownReason?: CooldownReason;
|
|
58
62
|
readonly serverHint?: NumericServerHint;
|
|
59
63
|
}
|
|
@@ -248,6 +252,14 @@ export function classifyFailure(
|
|
|
248
252
|
return { category: "config", accountAction: "route-without-retry" };
|
|
249
253
|
}
|
|
250
254
|
|
|
255
|
+
if (signal.code === "refusal" || signal.code === "unknown_stop") {
|
|
256
|
+
return {
|
|
257
|
+
category: "unknown",
|
|
258
|
+
kind: signal.code === "refusal" ? "refusal" : "unknown-stop",
|
|
259
|
+
accountAction: "retain-account",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
251
263
|
if (isTransportFailure(signal)) {
|
|
252
264
|
return cooldown("transport", "transport", signal);
|
|
253
265
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isContextOverflow,
|
|
3
|
+
isRetryableAssistantError,
|
|
4
|
+
type AssistantMessage,
|
|
5
|
+
} from "@earendil-works/pi-ai";
|
|
6
|
+
import { sanitizeDiagnosticText } from "./diagnostics.js";
|
|
7
|
+
|
|
8
|
+
/** Fixed fallbacks for a structured provider stop; none matches a host re-dispatch pattern. */
|
|
9
|
+
export const REFUSAL_FALLBACK_MESSAGE = "The model refused to complete the request";
|
|
10
|
+
export const UNKNOWN_STOP_FALLBACK_MESSAGE =
|
|
11
|
+
"Provider stopped with an unrecognized stop reason";
|
|
12
|
+
|
|
13
|
+
/** Upper bound on the normalized stop reason named in a reworded message. */
|
|
14
|
+
const MAX_NAMED_REASON_LENGTH = 64;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether the pinned host would re-dispatch an error terminal carrying `text`.
|
|
18
|
+
*
|
|
19
|
+
* The host re-dispatches when either retry predicate matches the prose:
|
|
20
|
+
* `AgentSession._isRetryableError` runs pi-ai `isRetryableAssistantError`, and
|
|
21
|
+
* `_checkCompaction` runs `isContextOverflow` before compacting and retrying once
|
|
22
|
+
* (pi-coding-agent dist/core/agent-session.js, both from `@earendil-works/pi-ai`).
|
|
23
|
+
* An unreadable predicate result counts as a match, so the caller keeps looking
|
|
24
|
+
* for a safer form.
|
|
25
|
+
*/
|
|
26
|
+
function hostWouldRedispatch(message: AssistantMessage, text: string): boolean {
|
|
27
|
+
try {
|
|
28
|
+
const probe = { ...message, errorMessage: text };
|
|
29
|
+
return isRetryableAssistantError(probe) || isContextOverflow(probe, 0);
|
|
30
|
+
} catch {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function namedReasonMessage(reason: string): string {
|
|
36
|
+
return `Provider stopped (reason: ${reason})`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalizes a bounded, sanitized raw stop reason into plain words: every run of
|
|
41
|
+
* non-alphanumeric characters (underscores included) becomes one space, and the
|
|
42
|
+
* result is capped at a word boundary where possible.
|
|
43
|
+
*/
|
|
44
|
+
function normalizedReasonWords(rawStopReason: unknown): string[] {
|
|
45
|
+
if (typeof rawStopReason !== "string") return [];
|
|
46
|
+
const words = sanitizeDiagnosticText(rawStopReason)
|
|
47
|
+
.replace(/[^A-Za-z0-9]+/g, " ")
|
|
48
|
+
.trim()
|
|
49
|
+
.split(" ")
|
|
50
|
+
.filter((word) => word.length > 0);
|
|
51
|
+
const kept: string[] = [];
|
|
52
|
+
let length = 0;
|
|
53
|
+
for (const word of words) {
|
|
54
|
+
const next = length === 0 ? word.length : length + 1 + word.length;
|
|
55
|
+
if (next > MAX_NAMED_REASON_LENGTH) {
|
|
56
|
+
if (kept.length === 0) kept.push(word.slice(0, MAX_NAMED_REASON_LENGTH));
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
kept.push(word);
|
|
60
|
+
length = next;
|
|
61
|
+
}
|
|
62
|
+
return kept;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Names an unknown stop reason in a message neither host predicate acts on.
|
|
67
|
+
*
|
|
68
|
+
* The first form keeps every normalized word. When that still matches a host
|
|
69
|
+
* predicate (a reason containing "overloaded" or "timeout", say), the second
|
|
70
|
+
* form admits words in order and drops each one whose addition would make the
|
|
71
|
+
* message match. Every admitted prefix was probed, so the result is host-final
|
|
72
|
+
* by construction. An empty result yields `undefined`.
|
|
73
|
+
*/
|
|
74
|
+
function unknownStopNamingReason(
|
|
75
|
+
message: AssistantMessage,
|
|
76
|
+
rawStopReason: unknown,
|
|
77
|
+
): string | undefined {
|
|
78
|
+
const words = normalizedReasonWords(rawStopReason);
|
|
79
|
+
if (words.length === 0) return undefined;
|
|
80
|
+
const reworded = namedReasonMessage(words.join(" "));
|
|
81
|
+
if (!hostWouldRedispatch(message, reworded)) return reworded;
|
|
82
|
+
const kept: string[] = [];
|
|
83
|
+
for (const word of words) {
|
|
84
|
+
const tentative = namedReasonMessage([...kept, word].join(" "));
|
|
85
|
+
if (!hostWouldRedispatch(message, tentative)) kept.push(word);
|
|
86
|
+
}
|
|
87
|
+
return kept.length === 0 ? undefined : namedReasonMessage(kept.join(" "));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The public error text for a structured refusal or unknown provider stop.
|
|
92
|
+
*
|
|
93
|
+
* The pinned host re-dispatches an error terminal from its prose alone (see
|
|
94
|
+
* `hostWouldRedispatch`). A refusal explanation or stop reason is
|
|
95
|
+
* provider-authored, so text such as "overloaded", "prompt is too long", or
|
|
96
|
+
* "model_context_window_exceeded" would make the host send the stopped request
|
|
97
|
+
* again. The structured code alone selects this path; the provider text is kept
|
|
98
|
+
* only when neither host predicate would act on it. An unknown stop is then
|
|
99
|
+
* reworded so it still names its reason, and only when no reworded form is safe
|
|
100
|
+
* is a fixed fallback published. A refusal whose explanation is unsafe publishes
|
|
101
|
+
* its fixed fallback directly. A message without the structured code, or that is
|
|
102
|
+
* not an error terminal, yields `undefined` and must be published unchanged.
|
|
103
|
+
*/
|
|
104
|
+
export function hostFinalStopMessage(
|
|
105
|
+
message: AssistantMessage,
|
|
106
|
+
): { readonly errorMessage: string } | undefined {
|
|
107
|
+
const code = (message as { code?: unknown }).code;
|
|
108
|
+
if (message.stopReason !== "error") return undefined;
|
|
109
|
+
if (code !== "refusal" && code !== "unknown_stop") return undefined;
|
|
110
|
+
const candidate = message.errorMessage;
|
|
111
|
+
if (
|
|
112
|
+
typeof candidate === "string" &&
|
|
113
|
+
candidate.length > 0 &&
|
|
114
|
+
!hostWouldRedispatch(message, candidate)
|
|
115
|
+
) {
|
|
116
|
+
return { errorMessage: candidate };
|
|
117
|
+
}
|
|
118
|
+
if (code === "refusal") return { errorMessage: REFUSAL_FALLBACK_MESSAGE };
|
|
119
|
+
return {
|
|
120
|
+
errorMessage:
|
|
121
|
+
unknownStopNamingReason(message, message.rawStopReason) ??
|
|
122
|
+
UNKNOWN_STOP_FALLBACK_MESSAGE,
|
|
123
|
+
};
|
|
124
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { join } from "node:path";
|
|
6
5
|
import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
|
|
7
6
|
import { readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
8
7
|
import type {
|
|
@@ -29,6 +28,10 @@ import {
|
|
|
29
28
|
createOpenAiAliasProviderConfig,
|
|
30
29
|
openaiPlatformModels,
|
|
31
30
|
} from "./openai-adapter.js";
|
|
31
|
+
import {
|
|
32
|
+
loadMaintainedCodexStream,
|
|
33
|
+
resolveInstalledPiAiVersion,
|
|
34
|
+
} from "./pi-ai-entrypoints.js";
|
|
32
35
|
import {
|
|
33
36
|
ALLOWED_FAMILIES,
|
|
34
37
|
DEFAULT_CONFIG,
|
|
@@ -405,6 +408,16 @@ function readInstalledDeclaration(targetPath: string): unknown {
|
|
|
405
408
|
}
|
|
406
409
|
}
|
|
407
410
|
|
|
411
|
+
/** The session's id, or undefined when the host context cannot supply one. */
|
|
412
|
+
function readSessionId(ctx: ExtensionContext): string | undefined {
|
|
413
|
+
try {
|
|
414
|
+
const id = (ctx.sessionManager as { getSessionId?: () => unknown }).getSessionId?.();
|
|
415
|
+
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
416
|
+
} catch {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
408
421
|
/** The complete declaration-gated startup seam, exported for causal registration contracts. */
|
|
409
422
|
export function registerLogicalProviderAtMatchedStartup(input: {
|
|
410
423
|
pi: Parameters<typeof registerLogicalProviderForSession>[0];
|
|
@@ -413,6 +426,8 @@ export function registerLogicalProviderAtMatchedStartup(input: {
|
|
|
413
426
|
deps: LogicalProviderDeps;
|
|
414
427
|
attributionStoreFactory?: () => LogicalAttributionLifecycle;
|
|
415
428
|
registerLogicalApi?: LogicalApiRegistrar;
|
|
429
|
+
/** The registering session's id; routes `unified` calls back to their caller. */
|
|
430
|
+
sessionId?: string | undefined;
|
|
416
431
|
}): {
|
|
417
432
|
status: ReturnType<typeof inspectInstalledDeclaration>;
|
|
418
433
|
expectedModels: ReturnType<typeof buildModelDeclaration>["models"];
|
|
@@ -424,6 +439,7 @@ export function registerLogicalProviderAtMatchedStartup(input: {
|
|
|
424
439
|
deps,
|
|
425
440
|
attributionStoreFactory,
|
|
426
441
|
registerLogicalApi,
|
|
442
|
+
sessionId,
|
|
427
443
|
} = input;
|
|
428
444
|
const expected = buildModelDeclaration(liveCatalogs);
|
|
429
445
|
const status = inspectInstalledDeclaration(installed, expected.models);
|
|
@@ -442,6 +458,7 @@ export function registerLogicalProviderAtMatchedStartup(input: {
|
|
|
442
458
|
expected.models,
|
|
443
459
|
attributedDeps,
|
|
444
460
|
registerLogicalApi,
|
|
461
|
+
sessionId === undefined ? {} : { sessionId },
|
|
445
462
|
);
|
|
446
463
|
}
|
|
447
464
|
return { status, expectedModels: expected.models };
|
|
@@ -1242,24 +1259,6 @@ function continuationReason(
|
|
|
1242
1259
|
}
|
|
1243
1260
|
}
|
|
1244
1261
|
|
|
1245
|
-
async function loadMaintainedCodexStream(): Promise<
|
|
1246
|
-
NonNullable<ProviderConfig["streamSimple"]>
|
|
1247
|
-
> {
|
|
1248
|
-
const publicRoot = await import("@earendil-works/pi-ai");
|
|
1249
|
-
const compatibilityStream = (
|
|
1250
|
-
publicRoot as unknown as Record<string, unknown>
|
|
1251
|
-
)["streamSimpleOpenAICodexResponses"];
|
|
1252
|
-
if (typeof compatibilityStream === "function") {
|
|
1253
|
-
return compatibilityStream as NonNullable<ProviderConfig["streamSimple"]>;
|
|
1254
|
-
}
|
|
1255
|
-
const maintainedApi = await import(
|
|
1256
|
-
"@earendil-works/pi-ai/api/openai-codex-responses"
|
|
1257
|
-
);
|
|
1258
|
-
return maintainedApi.streamSimple as unknown as NonNullable<
|
|
1259
|
-
ProviderConfig["streamSimple"]
|
|
1260
|
-
>;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
1262
|
/**
|
|
1264
1263
|
* Fail-closed sentinel used only when the host codex OAuth surface is
|
|
1265
1264
|
* unavailable at session_start (host shape drift). Its OAuth callbacks throw a
|
|
@@ -2065,50 +2064,6 @@ function cacheInheritedAccountGroupResolution(
|
|
|
2065
2064
|
}
|
|
2066
2065
|
|
|
2067
2066
|
|
|
2068
|
-
/**
|
|
2069
|
-
* Resolves the actually-installed `@earendil-works/pi-ai` package version by
|
|
2070
|
-
* walking up from its resolved `providers/anthropic.models` module file to
|
|
2071
|
-
* the nearest `package.json` whose `name` matches. A local, bounded,
|
|
2072
|
-
* offline disk read -- never a network request -- so the report's recorded
|
|
2073
|
-
* pricing-method provenance names whichever pinned catalog this process
|
|
2074
|
-
* actually loaded instead of a hard-coded string that could drift from it.
|
|
2075
|
-
* Shares its shape with the standalone CLI's own offline resolver in
|
|
2076
|
-
* `standalone-cli.ts`; the two adapters read from different sources (a live
|
|
2077
|
-
* `ExtensionContext.modelRegistry` here, the pinned package directly there)
|
|
2078
|
-
* and so cannot share one function without coupling this extension entry
|
|
2079
|
-
* point to the separate standalone-CLI module.
|
|
2080
|
-
*/
|
|
2081
|
-
function resolveInstalledPiAiVersion(): string {
|
|
2082
|
-
try {
|
|
2083
|
-
const moduleUrl = import.meta.resolve(
|
|
2084
|
-
"@earendil-works/pi-ai/providers/anthropic.models",
|
|
2085
|
-
);
|
|
2086
|
-
let directory = dirname(fileURLToPath(moduleUrl));
|
|
2087
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
2088
|
-
try {
|
|
2089
|
-
const candidate = JSON.parse(
|
|
2090
|
-
readFileSync(join(directory, "package.json"), "utf-8"),
|
|
2091
|
-
) as { readonly name?: unknown; readonly version?: unknown };
|
|
2092
|
-
if (
|
|
2093
|
-
candidate.name === "@earendil-works/pi-ai" &&
|
|
2094
|
-
typeof candidate.version === "string" &&
|
|
2095
|
-
candidate.version.length > 0
|
|
2096
|
-
) {
|
|
2097
|
-
return `pi-ai@${candidate.version}`;
|
|
2098
|
-
}
|
|
2099
|
-
} catch {
|
|
2100
|
-
// Keep walking toward the installed package root.
|
|
2101
|
-
}
|
|
2102
|
-
const parent = dirname(directory);
|
|
2103
|
-
if (parent === directory) break;
|
|
2104
|
-
directory = parent;
|
|
2105
|
-
}
|
|
2106
|
-
} catch {
|
|
2107
|
-
// Resolution failure falls through to the bounded fallback below.
|
|
2108
|
-
}
|
|
2109
|
-
return "pi-ai@unknown";
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
2067
|
/**
|
|
2113
2068
|
* Builds the extension surfaces' live Pi-catalog snapshot from the session's
|
|
2114
2069
|
* real `ExtensionContext.modelRegistry.getAll()` -- the same public,
|
|
@@ -3935,6 +3890,18 @@ export const createMultiAccountExtension =
|
|
|
3935
3890
|
modelSupport,
|
|
3936
3891
|
alreadyCooled: classified.alreadyCooled === true,
|
|
3937
3892
|
});
|
|
3893
|
+
if (decision.status === "retained") {
|
|
3894
|
+
// A structured refusal or unknown stop keeps the account: no switch,
|
|
3895
|
+
// no park, no OpenRouter, and no continuation. The same context would
|
|
3896
|
+
// stop the same way on any account.
|
|
3897
|
+
diagnostics.record(
|
|
3898
|
+
"info",
|
|
3899
|
+
"routing.retained",
|
|
3900
|
+
"The provider stopped the turn with a refusal or unknown stop reason; the account was kept and no follow-up was scheduled.",
|
|
3901
|
+
{ providerId, kind: decision.classification.kind ?? "unknown" },
|
|
3902
|
+
);
|
|
3903
|
+
return;
|
|
3904
|
+
}
|
|
3938
3905
|
if (decision.status === "paused") {
|
|
3939
3906
|
// OpenRouter is an explicitly enabled, metered FINAL rung. The helper
|
|
3940
3907
|
// re-checks every managed family so family-chain policy cannot bypass
|
|
@@ -4531,6 +4498,8 @@ export const createMultiAccountExtension =
|
|
|
4531
4498
|
installed,
|
|
4532
4499
|
liveCatalogs,
|
|
4533
4500
|
registerLogicalApi,
|
|
4501
|
+
// Optional routing key; an unreadable id must never block registration.
|
|
4502
|
+
sessionId: readSessionId(startupContext),
|
|
4534
4503
|
attributionStoreFactory: () => {
|
|
4535
4504
|
const store = createAttributionStore({
|
|
4536
4505
|
onResponse: (route, response) => {
|
|
@@ -4916,7 +4885,7 @@ export const createMultiAccountExtension =
|
|
|
4916
4885
|
config,
|
|
4917
4886
|
nowMs,
|
|
4918
4887
|
});
|
|
4919
|
-
if (reactive.status
|
|
4888
|
+
if (reactive.status !== "selected") return;
|
|
4920
4889
|
reactiveCandidate = reactive;
|
|
4921
4890
|
destinationProviderId = reactive.destination.providerId;
|
|
4922
4891
|
}
|
|
@@ -5342,9 +5311,17 @@ export const createMultiAccountExtension =
|
|
|
5342
5311
|
turnRouteOrigin = origin;
|
|
5343
5312
|
handledFailures.add(event.message);
|
|
5344
5313
|
const responseFailure = lastFailure.get(providerId) ?? {};
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5314
|
+
// A structured provider stop is copied only from this two-value allowlist,
|
|
5315
|
+
// never spread, and it wins over any code parsed from provider-authored
|
|
5316
|
+
// errorMessage prose: refusal text must not read as a rate limit.
|
|
5317
|
+
const rawStopCode = (event.message as { code?: unknown }).code;
|
|
5318
|
+
const stopCode =
|
|
5319
|
+
rawStopCode === "refusal" || rawStopCode === "unknown_stop"
|
|
5320
|
+
? rawStopCode
|
|
5321
|
+
: undefined;
|
|
5322
|
+
const messageCode =
|
|
5323
|
+
stopCode ??
|
|
5324
|
+
providerErrorCodeFromMessage(event.message.errorMessage);
|
|
5348
5325
|
const failure: ProviderFailureSignal = {
|
|
5349
5326
|
...responseFailure,
|
|
5350
5327
|
...(messageCode === undefined ? {} : { code: messageCode }),
|
package/src/logical-provider.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type SimpleStreamOptions,
|
|
23
23
|
} from "@earendil-works/pi-ai";
|
|
24
24
|
import { RuntimeState, type LogicalRoutePin } from "./runtime-state.js";
|
|
25
|
+
import { hostFinalStopMessage } from "./host-final-stop-message.js";
|
|
25
26
|
import { DEFAULT_CONFIG } from "./config.js";
|
|
26
27
|
import type {
|
|
27
28
|
AllowedFamily,
|
|
@@ -47,6 +48,16 @@ import type { ProviderType, Vendor } from "./vendor.js";
|
|
|
47
48
|
|
|
48
49
|
export { LOGICAL_PROVIDER_ID } from "./models-declaration.js";
|
|
49
50
|
import { LOGICAL_PROVIDER_ID } from "./models-declaration.js";
|
|
51
|
+
import { forceCodexSseOptions } from "./codex-adapter.js";
|
|
52
|
+
|
|
53
|
+
/** Fixed diagnostic label for a caller-supplied transport; never echoes the raw value. */
|
|
54
|
+
function codexTransportLabel(
|
|
55
|
+
transport: unknown,
|
|
56
|
+
): "websocket" | "websocket-cached" | "auto" | "other" {
|
|
57
|
+
return transport === "websocket" || transport === "websocket-cached" || transport === "auto"
|
|
58
|
+
? transport
|
|
59
|
+
: "other";
|
|
60
|
+
}
|
|
50
61
|
|
|
51
62
|
/** One physical account the logical provider may dispatch to. */
|
|
52
63
|
export interface LogicalPhysicalAccount {
|
|
@@ -666,6 +677,7 @@ export function createLogicalProvider(
|
|
|
666
677
|
deps: LogicalProviderDeps,
|
|
667
678
|
): LogicalProvider {
|
|
668
679
|
const coordinator = createHostRetryCoordinator(deps);
|
|
680
|
+
let codexTransportNoticeSent = false;
|
|
669
681
|
|
|
670
682
|
const diagnose = (message: string): void => {
|
|
671
683
|
deps.onDiagnostic?.(message);
|
|
@@ -936,6 +948,7 @@ export function createLogicalProvider(
|
|
|
936
948
|
|
|
937
949
|
const projectMessage = (message: AssistantMessage, modelId: string): AssistantMessage => ({
|
|
938
950
|
...message, api: LOGICAL_PROVIDER_ID, provider: LOGICAL_PROVIDER_ID, model: modelId,
|
|
951
|
+
...hostFinalStopMessage(message),
|
|
939
952
|
});
|
|
940
953
|
|
|
941
954
|
const projectEvent = (event: unknown, modelId: string): unknown => {
|
|
@@ -1127,8 +1140,24 @@ export function createLogicalProvider(
|
|
|
1127
1140
|
await originalOnResponse(response, responseModel);
|
|
1128
1141
|
}
|
|
1129
1142
|
};
|
|
1143
|
+
let routedOptions = options;
|
|
1144
|
+
if (account.family === "openai-codex") {
|
|
1145
|
+
// Temporary Pi 0.99 WebSocket containment; see CODEX_FORCED_TRANSPORT.
|
|
1146
|
+
const forced = forceCodexSseOptions(options);
|
|
1147
|
+
routedOptions = forced.options;
|
|
1148
|
+
if (forced.overridden && options?.transport !== undefined && !codexTransportNoticeSent) {
|
|
1149
|
+
codexTransportNoticeSent = true;
|
|
1150
|
+
try {
|
|
1151
|
+
deps.onDiagnostic?.(
|
|
1152
|
+
`Codex transport "${codexTransportLabel(options.transport)}" overridden to "sse" on routed calls (Pi 0.99 WebSocket containment).`,
|
|
1153
|
+
);
|
|
1154
|
+
} catch {
|
|
1155
|
+
// A diagnostic sink failure cannot replace a provider result.
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1130
1159
|
const attributedOptions: SimpleStreamOptions = {
|
|
1131
|
-
...
|
|
1160
|
+
...routedOptions,
|
|
1132
1161
|
onPayload: wrappedOnPayload,
|
|
1133
1162
|
onResponse: wrappedOnResponse,
|
|
1134
1163
|
};
|