@centerforagenticai/pi-multi-account 0.1.5 → 0.1.7

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.
@@ -88,6 +88,16 @@ export type ManagedLogicalModelSource =
88
88
  readonly model: Model<typeof GOOGLE_ANTIGRAVITY_API>;
89
89
  };
90
90
 
91
+ /**
92
+ * Closed copy-by-name tables over Pi's model types. A field Pi removes or
93
+ * renames is a compile error (excess property), so a lost capability is never
94
+ * silent. A field Pi adds is NOT required here: it is excluded by default
95
+ * because projection copies only listed fields, and
96
+ * `test/package-contract.test.ts` reports it as a warning for review. See
97
+ * docs/pi-upgrades.md.
98
+ */
99
+ type PiFieldDisposition<T, D extends string> = Partial<Record<keyof T, D>>;
100
+
91
101
  const MODEL_FIELD_DISPOSITION = {
92
102
  id: "copy",
93
103
  name: "copy",
@@ -103,8 +113,8 @@ const MODEL_FIELD_DISPOSITION = {
103
113
  samplingParams: "copy",
104
114
  headers: "exclude",
105
115
  compat: "copy",
106
- } as const satisfies Record<
107
- keyof Model<Api>,
116
+ } as const satisfies PiFieldDisposition<
117
+ Model<Api>,
108
118
  "copy" | "exclude" | "override"
109
119
  >;
110
120
 
@@ -118,9 +128,8 @@ const ANTHROPIC_COMPAT_DISPOSITION = {
118
128
  allowEmptySignature: "copy",
119
129
  supportsStrictTools: "copy",
120
130
  allowedFallbackModels: "exclude",
121
- supportsToolReferences: "copy",
122
- } as const satisfies Record<
123
- keyof AnthropicMessagesCompat,
131
+ } as const satisfies PiFieldDisposition<
132
+ AnthropicMessagesCompat,
124
133
  "copy" | "exclude"
125
134
  >;
126
135
 
@@ -133,7 +142,7 @@ const CODEX_COMPAT_DISPOSITION = {
133
142
  supportsAdditionalTools: "copy",
134
143
  supportsToolSearch: "copy",
135
144
  supportsExplicitPromptCacheMode: "copy",
136
- } as const satisfies Record<keyof OpenAIResponsesCompat, "copy">;
145
+ } as const satisfies PiFieldDisposition<OpenAIResponsesCompat, "copy">;
137
146
 
138
147
  const THINKING_LEVEL_DISPOSITION = {
139
148
  off: "copy",
@@ -549,8 +558,6 @@ function projectAnthropicCompat(
549
558
  if (emptySignature !== undefined) projected.allowEmptySignature = emptySignature;
550
559
  const strictTools = optionalCompatBoolean(properties, "supportsStrictTools", "anthropic");
551
560
  if (strictTools !== undefined) projected.supportsStrictTools = strictTools;
552
- const toolReferences = optionalCompatBoolean(properties, "supportsToolReferences", "anthropic");
553
- if (toolReferences !== undefined) projected.supportsToolReferences = toolReferences;
554
561
  return projected;
555
562
  }
556
563
 
@@ -0,0 +1,163 @@
1
+ import type { Api, Model, AssistantMessage, AssistantMessageEvent, ToolCall } from "@earendil-works/pi-ai";
2
+ import { PROVIDER_ERROR_CODES, TRANSPORT_FAILURE_KINDS, type ProviderErrorCode, type TransportFailureKind } from "./error-classification.js";
3
+ import { hostFinalStopMessage } from "./host-final-stop-message.js";
4
+ import { sanitizeDiagnosticText } from "./diagnostics.js";
5
+
6
+ export interface PublicAssistantIdentity {
7
+ readonly api: AssistantMessage["api"];
8
+ readonly provider: AssistantMessage["provider"];
9
+ readonly model: string;
10
+ }
11
+ export type PublicAssistantMessage = AssistantMessage & {
12
+ code?: ProviderErrorCode;
13
+ httpStatus?: number;
14
+ transportKind?: TransportFailureKind;
15
+ retryAfterSeconds?: number;
16
+ resetAtMs?: number;
17
+ };
18
+
19
+ /** Bounds accounting/time facts without coercing provider values. */
20
+ export function finiteNonNegative(value: unknown): number | undefined {
21
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER ? value : undefined;
22
+ }
23
+ export function projectTerminalUsage(message: AssistantMessage): AssistantMessage["usage"] | undefined {
24
+ const usage = message.usage;
25
+ if (typeof usage !== "object" || usage === null || typeof usage.cost !== "object" || usage.cost === null) return undefined;
26
+ const input = finiteNonNegative(usage.input);
27
+ const output = finiteNonNegative(usage.output);
28
+ const cacheRead = finiteNonNegative(usage.cacheRead);
29
+ const cacheWrite = finiteNonNegative(usage.cacheWrite);
30
+ const totalTokens = finiteNonNegative(usage.totalTokens);
31
+ const costInput = finiteNonNegative(usage.cost.input);
32
+ const costOutput = finiteNonNegative(usage.cost.output);
33
+ const costCacheRead = finiteNonNegative(usage.cost.cacheRead);
34
+ const costCacheWrite = finiteNonNegative(usage.cost.cacheWrite);
35
+ const costTotal = finiteNonNegative(usage.cost.total);
36
+ const cacheWrite1h = finiteNonNegative(usage.cacheWrite1h);
37
+ const reasoning = finiteNonNegative(usage.reasoning);
38
+ if ((usage.cacheWrite1h !== undefined && cacheWrite1h === undefined) || (usage.reasoning !== undefined && reasoning === undefined)) return undefined;
39
+ // Construct only validated named accounting fields.
40
+ if (input === undefined || output === undefined || cacheRead === undefined || cacheWrite === undefined || totalTokens === undefined || costInput === undefined || costOutput === undefined || costCacheRead === undefined || costCacheWrite === undefined || costTotal === undefined) return undefined;
41
+ return { input, output, cacheRead, cacheWrite, totalTokens,
42
+ ...(cacheWrite1h === undefined ? {} : { cacheWrite1h }), ...(reasoning === undefined ? {} : { reasoning }),
43
+ cost: { input: costInput, output: costOutput, cacheRead: costCacheRead, cacheWrite: costCacheWrite, total: costTotal } };
44
+ }
45
+
46
+ function projectToolCall(block: ToolCall): ToolCall {
47
+ if (typeof block !== "object" || block === null || typeof block.id !== "string" || typeof block.name !== "string" || typeof block.arguments !== "object" || block.arguments === null || Array.isArray(block.arguments)) {
48
+ throw new TypeError("Malformed public tool call.");
49
+ }
50
+ return { type: "toolCall", id: block.id, name: block.name, arguments: block.arguments,
51
+ ...(typeof block.thoughtSignature === "string" ? { thoughtSignature: block.thoughtSignature } : {}),
52
+ ...(typeof block.namespace === "string" ? { namespace: block.namespace } : {}) };
53
+ }
54
+ /** Content is the response, not diagnostic metadata. Keep protocol signatures and tool IDs. */
55
+ export function projectAssistantContent(content: AssistantMessage["content"]): AssistantMessage["content"] {
56
+ if (!Array.isArray(content)) return [];
57
+ return content.flatMap((block): AssistantMessage["content"] => {
58
+ if (typeof block !== "object" || block === null) return [];
59
+ switch (block.type) {
60
+ case "text": if (typeof block.text !== "string") return [];
61
+ return [{ type: "text", text: block.text,
62
+ ...(typeof block.textSignature === "string" ? { textSignature: block.textSignature } : {}) }];
63
+ case "thinking": if (typeof block.thinking !== "string") return [];
64
+ return [{ type: "thinking", thinking: block.thinking,
65
+ ...(typeof block.thinkingSignature === "string" ? { thinkingSignature: block.thinkingSignature } : {}),
66
+ ...(typeof block.redacted === "boolean" ? { redacted: block.redacted } : {}) }];
67
+ case "toolCall":
68
+ if (typeof block.id !== "string" || typeof block.name !== "string" || typeof block.arguments !== "object" || block.arguments === null || Array.isArray(block.arguments)) return [];
69
+ return [projectToolCall(block)];
70
+ default: return [];
71
+ }
72
+ });
73
+ }
74
+
75
+ /** Only the managed transport diagnostic's named facts are public; never its Error/details DTO. */
76
+ function projectTransportDiagnostics(message: AssistantMessage): AssistantMessage["diagnostics"] {
77
+ if (!Array.isArray(message.diagnostics)) return undefined;
78
+ return message.diagnostics.flatMap((entry) => {
79
+ if (typeof entry !== "object" || entry === null || entry.type !== "provider_transport_failure") return [];
80
+ const timestamp = finiteNonNegative(entry.timestamp);
81
+ if (timestamp === undefined) return [];
82
+ const details = entry.details;
83
+ const configured = details?.configuredTransport;
84
+ const fallback = details?.fallbackTransport;
85
+ const phase = details?.phase;
86
+ return [{ type: "provider_transport_failure", timestamp, details: {
87
+ ...(typeof configured === "string" && ["auto", "sse", "websocket", "websocket-cached"].includes(configured) ? { configuredTransport: configured } : {}),
88
+ ...(fallback === "sse" ? { fallbackTransport: "sse" } : {}),
89
+ ...(typeof details?.eventsEmitted === "boolean" ? { eventsEmitted: details.eventsEmitted } : {}),
90
+ ...(phase === "before_message_stream_start" || phase === "after_message_stream_start" ? { phase } : {}),
91
+ } }];
92
+ });
93
+ }
94
+
95
+ // Known wire reasons are public enums, not arbitrary provider prose. Managed streams
96
+ // emit no DeferredHandle; responseId/responseModel are output-only metadata here.
97
+ const PUBLIC_RAW_STOP_REASONS = [
98
+ "end_turn", "max_tokens", "stop_sequence", "tool_use", "pause_turn", "refusal", "sensitive",
99
+ "completed", "incomplete", "incomplete.max_output_tokens", "incomplete.content_filter", "content_filter", "failed", "cancelled",
100
+ "STOP", "MAX_TOKENS", "SAFETY", "PROHIBITED_CONTENT", "BLOCKLIST", "SPII", "RECITATION", "FINISH_REASON_UNSPECIFIED", "OTHER",
101
+ ] as const;
102
+ /** Public managed response DTO: no provider-object spread, request IDs, raw bodies or unknown fields. */
103
+ export function projectPublicAssistantMessage(
104
+ message: AssistantMessage, identity: PublicAssistantIdentity, errorMessage?: string,
105
+ ): PublicAssistantMessage {
106
+ const stop = hostFinalStopMessage(message);
107
+ const codeValue = Object.getOwnPropertyDescriptor(message, "code")?.value;
108
+ const code = PROVIDER_ERROR_CODES.find((value) => value === codeValue);
109
+ const statusValue = Object.getOwnPropertyDescriptor(message, "httpStatus")?.value ?? Object.getOwnPropertyDescriptor(message, "status")?.value;
110
+ const httpStatus = typeof statusValue === "number" && Number.isInteger(statusValue) && statusValue >= 100 && statusValue <= 599 ? statusValue : undefined;
111
+ const transportValue = Object.getOwnPropertyDescriptor(message, "transportKind")?.value;
112
+ const transportKind = TRANSPORT_FAILURE_KINDS.find((value) => value === transportValue);
113
+ const retryAfterSeconds = finiteNonNegative(Object.getOwnPropertyDescriptor(message, "retryAfterSeconds")?.value);
114
+ const resetAtMs = finiteNonNegative(Object.getOwnPropertyDescriptor(message, "resetAtMs")?.value);
115
+ const diagnostics = projectTransportDiagnostics(message);
116
+ const usage = projectTerminalUsage(message) ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
117
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
118
+ return {
119
+ role: "assistant", content: projectAssistantContent(message.content),
120
+ api: identity.api, provider: identity.provider, model: identity.model,
121
+ usage, timestamp: finiteNonNegative(message.timestamp) ?? 0,
122
+ stopReason: ["pending", "stop", "length", "toolUse", "error", "aborted", "deferred"].includes(message.stopReason) ? message.stopReason : "error",
123
+ ...(typeof message.endTurn === "boolean" ? { endTurn: message.endTurn } : {}),
124
+ ...(PUBLIC_RAW_STOP_REASONS.find((reason) => reason === message.rawStopReason) === undefined ? {} : { rawStopReason: message.rawStopReason }),
125
+ ...(code === undefined ? {} : { code }), ...(httpStatus === undefined ? {} : { httpStatus }),
126
+ ...(transportKind === undefined ? {} : { transportKind }),
127
+ ...(retryAfterSeconds === undefined ? {} : { retryAfterSeconds }), ...(resetAtMs === undefined ? {} : { resetAtMs }),
128
+ ...(diagnostics === undefined || diagnostics.length === 0 ? {} : { diagnostics }),
129
+ ...(stop ?? (errorMessage === undefined ? {} : { errorMessage })),
130
+ };
131
+ }
132
+
133
+ /** Named event variants prevent extra fields on progress and terminal envelopes from surviving. */
134
+ export function projectPublicAssistantEvent(event: unknown, identity: PublicAssistantIdentity, errorMessage?: string): AssistantMessageEvent | undefined {
135
+ if (typeof event !== "object" || event === null) return undefined;
136
+ // The public stream union is validated by its producer/registration bridge.
137
+ const source = event as AssistantMessageEvent;
138
+ const contentIndex = "contentIndex" in source && typeof source.contentIndex === "number" && Number.isSafeInteger(source.contentIndex) && source.contentIndex >= 0 ? source.contentIndex : 0;
139
+ switch (source.type) {
140
+ case "done": return { type: "done", reason: ["stop", "length", "toolUse", "deferred"].includes(source.reason) ? source.reason : "stop", message: projectPublicAssistantMessage(source.message, identity, errorMessage) };
141
+ case "error": return { type: "error", reason: source.reason === "aborted" ? "aborted" : "error", error: projectPublicAssistantMessage(source.error, identity, errorMessage) };
142
+ case "start": return { type: "start", partial: projectPublicAssistantMessage(source.partial, identity) };
143
+ case "text_start": case "thinking_start": case "toolcall_start":
144
+ return { type: source.type, contentIndex, partial: projectPublicAssistantMessage(source.partial, identity) };
145
+ case "text_delta": case "thinking_delta": case "toolcall_delta":
146
+ return { type: source.type, contentIndex, delta: typeof source.delta === "string" ? source.delta : "", partial: projectPublicAssistantMessage(source.partial, identity) };
147
+ case "text_end": case "thinking_end":
148
+ return { type: source.type, contentIndex, content: typeof source.content === "string" ? source.content : "", partial: projectPublicAssistantMessage(source.partial, identity) };
149
+ case "toolcall_end":
150
+ return { type: "toolcall_end", contentIndex, toolCall: projectToolCall(source.toolCall), partial: projectPublicAssistantMessage(source.partial, identity) };
151
+ default: return undefined;
152
+ }
153
+ }
154
+
155
+ /** Alias errors keep existing bounded retry prose; structured stops always use fixed reasons. */
156
+ export function projectAliasAssistantEvent(event: AssistantMessageEvent, model: Model<Api>): AssistantMessageEvent {
157
+ const rawError = event.type === "error" ? event.error.errorMessage : undefined;
158
+ const projected = projectPublicAssistantEvent(event, {
159
+ api: model.api, provider: model.provider, model: model.id,
160
+ }, rawError === undefined ? undefined : sanitizeDiagnosticText(rawError));
161
+ if (projected === undefined) throw new TypeError("Unsupported provider stream event.");
162
+ return projected;
163
+ }