@oh-my-pi/pi-ai 14.6.0 → 14.6.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "14.6.0",
4
+ "version": "14.6.1",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -46,8 +46,8 @@
46
46
  "@aws-sdk/credential-provider-node": "^3.972.36",
47
47
  "@bufbuild/protobuf": "^2.12.0",
48
48
  "@google/genai": "^1.50.1",
49
- "@oh-my-pi/pi-natives": "14.6.0",
50
- "@oh-my-pi/pi-utils": "14.6.0",
49
+ "@oh-my-pi/pi-natives": "14.6.1",
50
+ "@oh-my-pi/pi-utils": "14.6.1",
51
51
  "@sinclair/typebox": "^0.34.49",
52
52
  "@smithy/node-http-handler": "^4.6.1",
53
53
  "ajv": "^8.20.0",
package/src/models.json CHANGED
@@ -28105,6 +28105,44 @@
28105
28105
  "contextWindow": 222222,
28106
28106
  "maxTokens": 8888
28107
28107
  },
28108
+ "poolside/laguna-m.1": {
28109
+ "id": "poolside/laguna-m.1",
28110
+ "name": "poolside/laguna-m.1",
28111
+ "api": "openai-completions",
28112
+ "provider": "nanogpt",
28113
+ "baseUrl": "https://nano-gpt.com/api/v1",
28114
+ "reasoning": false,
28115
+ "input": [
28116
+ "text"
28117
+ ],
28118
+ "cost": {
28119
+ "input": 0,
28120
+ "output": 0,
28121
+ "cacheRead": 0,
28122
+ "cacheWrite": 0
28123
+ },
28124
+ "contextWindow": 222222,
28125
+ "maxTokens": 8888
28126
+ },
28127
+ "poolside/laguna-xs.2": {
28128
+ "id": "poolside/laguna-xs.2",
28129
+ "name": "poolside/laguna-xs.2",
28130
+ "api": "openai-completions",
28131
+ "provider": "nanogpt",
28132
+ "baseUrl": "https://nano-gpt.com/api/v1",
28133
+ "reasoning": false,
28134
+ "input": [
28135
+ "text"
28136
+ ],
28137
+ "cost": {
28138
+ "input": 0,
28139
+ "output": 0,
28140
+ "cacheRead": 0,
28141
+ "cacheWrite": 0
28142
+ },
28143
+ "contextWindow": 222222,
28144
+ "maxTokens": 8888
28145
+ },
28108
28146
  "qvq-max": {
28109
28147
  "id": "qvq-max",
28110
28148
  "name": "qvq-max",
@@ -35776,9 +35814,9 @@
35776
35814
  "minimax-m2.7": {
35777
35815
  "id": "minimax-m2.7",
35778
35816
  "name": "MiniMax M2.7",
35779
- "api": "anthropic-messages",
35817
+ "api": "openai-completions",
35780
35818
  "provider": "opencode-go",
35781
- "baseUrl": "https://opencode.ai/zen/go",
35819
+ "baseUrl": "https://opencode.ai/zen/go/v1",
35782
35820
  "reasoning": true,
35783
35821
  "input": [
35784
35822
  "text"
@@ -35792,7 +35830,7 @@
35792
35830
  "contextWindow": 204800,
35793
35831
  "maxTokens": 131072,
35794
35832
  "thinking": {
35795
- "mode": "budget",
35833
+ "mode": "effort",
35796
35834
  "minLevel": "minimal",
35797
35835
  "maxLevel": "xhigh"
35798
35836
  }
@@ -38485,7 +38523,7 @@
38485
38523
  "cacheRead": 0.024999999999999998,
38486
38524
  "cacheWrite": 0.08333333333333334
38487
38525
  },
38488
- "contextWindow": 1000000,
38526
+ "contextWindow": 1048576,
38489
38527
  "maxTokens": 8192
38490
38528
  },
38491
38529
  "google/gemini-2.0-flash-lite-001": {
@@ -42996,13 +43034,13 @@
42996
43034
  "image"
42997
43035
  ],
42998
43036
  "cost": {
42999
- "input": 0.325,
43000
- "output": 3.25,
43037
+ "input": 0.32,
43038
+ "output": 3.1999999999999997,
43001
43039
  "cacheRead": 0,
43002
43040
  "cacheWrite": 0
43003
43041
  },
43004
- "contextWindow": 256000,
43005
- "maxTokens": 65536,
43042
+ "contextWindow": 262144,
43043
+ "maxTokens": 81920,
43006
43044
  "thinking": {
43007
43045
  "mode": "effort",
43008
43046
  "minLevel": "minimal",
@@ -55034,6 +55072,26 @@
55034
55072
  "contextWindow": 2000000,
55035
55073
  "maxTokens": 30000
55036
55074
  },
55075
+ "x-ai/grok-4.3": {
55076
+ "id": "x-ai/grok-4.3",
55077
+ "name": "xAI: Grok 4.3",
55078
+ "api": "openai-completions",
55079
+ "provider": "zenmux",
55080
+ "baseUrl": "https://zenmux.ai/api/v1",
55081
+ "reasoning": false,
55082
+ "input": [
55083
+ "text",
55084
+ "image"
55085
+ ],
55086
+ "cost": {
55087
+ "input": 1.25,
55088
+ "output": 2.5,
55089
+ "cacheRead": 0.2,
55090
+ "cacheWrite": 0
55091
+ },
55092
+ "contextWindow": 1000000,
55093
+ "maxTokens": 8888
55094
+ },
55037
55095
  "x-ai/grok-code-fast-1": {
55038
55096
  "id": "x-ai/grok-code-fast-1",
55039
55097
  "name": "Grok Code Fast 1",
@@ -1868,12 +1868,18 @@ function createOpenCodeApiResolution(
1868
1868
  }
1869
1869
 
1870
1870
  const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen");
1871
- // OpenCode Go: models.dev declares qwen3.5-plus / qwen3.6-plus with
1872
- // `provider.npm = "@ai-sdk/anthropic"`, but per the OpenCode Go endpoint table
1873
- // (https://opencode.ai/docs/go/#endpoints) they are served via @ai-sdk/alibaba
1874
- // at https://opencode.ai/zen/go/v1/chat/completions (OpenAI-compatible).
1875
- // Override the resolver so regenerating models.json keeps the correct routing.
1871
+ // OpenCode Go: models.dev declares minimax-m2.7 / qwen3.5-plus / qwen3.6-plus
1872
+ // with `provider.npm = "@ai-sdk/anthropic"`, but the OpenCode Go gateway only
1873
+ // serves them at `https://opencode.ai/zen/go/v1/chat/completions` (verified
1874
+ // against https://opencode.ai/zen/go/v1/models and the upstream endpoint
1875
+ // table at https://opencode.ai/docs/go/#endpoints minimax-m2.5 works the
1876
+ // same way and lacks an `npm` field on models.dev so it already falls through
1877
+ // to the openai-completions default). Without this override the resolver
1878
+ // would POST anthropic-style requests to /v1/messages and the gateway would
1879
+ // return its `Page Not Found` HTML (issue #887). Override the resolver so
1880
+ // regenerating models.json keeps the correct routing.
1876
1881
  const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen/go", {
1882
+ "minimax-m2.7": "openai-completions",
1877
1883
  "qwen3.5-plus": "openai-completions",
1878
1884
  "qwen3.6-plus": "openai-completions",
1879
1885
  });
@@ -81,6 +81,43 @@ function normalizeMistralToolId(id: string, isMistral: boolean): string {
81
81
  return normalized;
82
82
  }
83
83
 
84
+ /**
85
+ * Normalize OpenAI-compatible streaming `delta.content` into plain text.
86
+ *
87
+ * Most providers stream `delta.content` as a string, but some (notably Mistral
88
+ * Medium 3.5 / `mistral-medium-2604`) return an array of typed content parts
89
+ * — e.g. `[{ type: "text", text: "Hello" }]`. Without normalization those
90
+ * parts get string-coerced via `text += array`, producing the literal
91
+ * `[object Object]` sequences observed in issue #911.
92
+ *
93
+ * Returns the joined text. Non-text parts and unknown shapes are skipped so
94
+ * we never emit JS object sigils as visible output.
95
+ */
96
+ function normalizeStreamingContentText(content: unknown): string {
97
+ if (typeof content === "string") return content;
98
+ if (Array.isArray(content)) {
99
+ let out = "";
100
+ for (const part of content) {
101
+ if (typeof part === "string") {
102
+ out += part;
103
+ } else if (part && typeof part === "object") {
104
+ const obj = part as { type?: unknown; text?: unknown };
105
+ if ((obj.type === undefined || obj.type === "text") && typeof obj.text === "string") {
106
+ out += obj.text;
107
+ }
108
+ }
109
+ }
110
+ return out;
111
+ }
112
+ if (content && typeof content === "object") {
113
+ const obj = content as { type?: unknown; text?: unknown };
114
+ if ((obj.type === undefined || obj.type === "text") && typeof obj.text === "string") {
115
+ return obj.text;
116
+ }
117
+ }
118
+ return "";
119
+ }
120
+
84
121
  function serializeToolArguments(value: unknown): string {
85
122
  if (value && typeof value === "object" && !Array.isArray(value)) {
86
123
  try {
@@ -537,6 +574,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
537
574
  idleTimeoutMs,
538
575
  errorMessage: "OpenAI completions stream stalled while waiting for the next event",
539
576
  onIdle: () => requestAbortController.abort(),
577
+ abortSignal: options?.signal,
540
578
  })) {
541
579
  if (!chunk || typeof chunk !== "object") continue;
542
580
 
@@ -567,20 +605,17 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
567
605
  }
568
606
 
569
607
  if (choice.delta) {
570
- if (
571
- choice.delta.content !== null &&
572
- choice.delta.content !== undefined &&
573
- choice.delta.content.length > 0
574
- ) {
608
+ const normalizedDeltaText = normalizeStreamingContentText(choice.delta.content);
609
+ if (normalizedDeltaText.length > 0) {
575
610
  if (!firstTokenTime) firstTokenTime = Date.now();
576
611
  if (parseMiniMaxThinkTags) {
577
- taggedTextBuffer += choice.delta.content;
612
+ taggedTextBuffer += normalizedDeltaText;
578
613
  flushTaggedTextBuffer();
579
614
  } else if (stripDeepseekChatTemplateTokens) {
580
- deepseekStripBuffer += choice.delta.content;
615
+ deepseekStripBuffer += normalizedDeltaText;
581
616
  flushDeepseekStripBuffer(false);
582
617
  } else {
583
- appendTextDelta(choice.delta.content);
618
+ appendTextDelta(normalizedDeltaText);
584
619
  }
585
620
  }
586
621
 
@@ -540,13 +540,26 @@ export async function processResponsesStream<TApi extends Api>(
540
540
  }
541
541
  calculateCost(model, output.usage);
542
542
  output.stopReason = mapOpenAIResponsesStopReason(response?.status);
543
+ if (response?.status === "failed" || response?.status === "cancelled") {
544
+ const error = response?.error ?? (response as any)?.status_details?.error;
545
+ const details = response?.incomplete_details;
546
+ const statusDetailsReason = (response as any)?.status_details?.reason;
547
+ const message = error
548
+ ? `${error.code || "unknown"}: ${error.message || "no message"}`
549
+ : details?.reason
550
+ ? `incomplete: ${details.reason}`
551
+ : typeof statusDetailsReason === "string" && statusDetailsReason.length > 0
552
+ ? `status_details: ${statusDetailsReason}`
553
+ : "Unknown error (no error details in response)";
554
+ throw new Error(message);
555
+ }
543
556
  if (output.content.some(block => block.type === "toolCall") && output.stopReason === "stop") {
544
557
  output.stopReason = "toolUse";
545
558
  }
546
559
  } else if (event.type === "error") {
547
560
  throw new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error");
548
561
  } else if (event.type === "response.failed") {
549
- const error = event.response?.error;
562
+ const error = event.response?.error ?? (event.response as any)?.status_details?.error;
550
563
  const details = event.response?.incomplete_details;
551
564
  const message = error
552
565
  ? `${error.code || "unknown"}: ${error.message || "no message"}`
@@ -220,6 +220,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
220
220
  watchdog: firstEventWatchdog,
221
221
  errorMessage: "OpenAI responses stream stalled while waiting for the next event",
222
222
  onIdle: () => requestAbortController.abort(),
223
+ abortSignal: options?.signal,
223
224
  }),
224
225
  output,
225
226
  stream,
@@ -132,8 +132,6 @@ interface BedrockProviderModule {
132
132
  // Module-level lazy promise caches
133
133
  // ---------------------------------------------------------------------------
134
134
 
135
- const importNodeOnlyProvider = (specifier: string): Promise<unknown> => import(specifier);
136
-
137
135
  let anthropicProviderModulePromise: Promise<LazyProviderModule<"anthropic-messages">> | undefined;
138
136
  let azureOpenAIResponsesProviderModulePromise: Promise<LazyProviderModule<"azure-openai-responses">> | undefined;
139
137
  let googleProviderModulePromise: Promise<LazyProviderModule<"google-generative-ai">> | undefined;
@@ -320,7 +318,7 @@ function loadBedrockProviderModule(): Promise<LazyProviderModule<"bedrock-conver
320
318
  if (bedrockProviderModuleOverride) {
321
319
  return Promise.resolve(bedrockProviderModuleOverride);
322
320
  }
323
- bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock").then(module => {
321
+ bedrockProviderModulePromise ||= import("./amazon-bedrock").then(module => {
324
322
  const provider = module as BedrockProviderModule;
325
323
  return { stream: provider.streamBedrock };
326
324
  });
@@ -59,6 +59,15 @@ export interface IdleTimeoutIteratorOptions {
59
59
  firstItemErrorMessage?: string;
60
60
  onIdle?: () => void;
61
61
  onFirstItemTimeout?: () => void;
62
+ /**
63
+ * Cancel iteration as soon as this signal aborts. Required for caller-driven
64
+ * cancellation (ESC) when the underlying transport does not surface signal
65
+ * aborts to the iterator (HTTP/2 proxies, native sockets, mocked fetch).
66
+ * Without this, the consumer sleeps on iterator.next() until the idle/first
67
+ * -event watchdog fires — observable as the issue #912 "Working… forever"
68
+ * symptom on the github-copilot provider.
69
+ */
70
+ abortSignal?: AbortSignal;
62
71
  }
63
72
 
64
73
  /**
@@ -73,19 +82,20 @@ export async function* iterateWithIdleTimeout<T>(
73
82
  ): AsyncGenerator<T> {
74
83
  let watchdog = options.watchdog;
75
84
  const firstItemTimeoutMs = options.firstItemTimeoutMs ?? options.idleTimeoutMs;
76
- if (
77
- (firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) &&
78
- (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0)
79
- ) {
80
- for await (const item of iterable) {
81
- watchdog && clearTimeout(watchdog);
82
- watchdog = undefined;
83
- yield item;
85
+ const abortSignal = options.abortSignal;
86
+ const iterator = iterable[Symbol.asyncIterator]();
87
+
88
+ const closeIterator = (): void => {
89
+ const returnPromise = iterator.return?.();
90
+ if (returnPromise) {
91
+ void returnPromise.catch(() => {});
84
92
  }
85
- return;
86
- }
93
+ };
87
94
 
88
- const iterator = iterable[Symbol.asyncIterator]();
95
+ if (abortSignal?.aborted) {
96
+ closeIterator();
97
+ throw abortReason(abortSignal);
98
+ }
89
99
 
90
100
  const withRacy = <T>(promise: Promise<T>) =>
91
101
  promise.then(
@@ -98,54 +108,83 @@ export async function* iterateWithIdleTimeout<T>(
98
108
  onFirst = null;
99
109
  };
100
110
 
111
+ const noTimeoutEnforced =
112
+ (firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) &&
113
+ (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0);
114
+
101
115
  while (true) {
102
116
  const nextResultPromise = withRacy(iterator.next());
103
117
  const activeTimeoutMs = !onFirst ? options.idleTimeoutMs : firstItemTimeoutMs;
104
118
 
105
- if (activeTimeoutMs === undefined || activeTimeoutMs <= 0) {
106
- const outcome = await nextResultPromise;
107
- if (outcome.kind === "error") {
108
- throw outcome.error;
109
- }
110
- if (outcome.result.done) {
111
- return;
112
- }
113
- onFirst?.();
114
- yield outcome.result.value;
115
- continue;
119
+ const racers: Array<
120
+ Promise<
121
+ | { kind: "next"; result: IteratorResult<T> }
122
+ | { kind: "error"; error: unknown }
123
+ | { kind: "timeout" }
124
+ | { kind: "abort" }
125
+ >
126
+ > = [nextResultPromise];
127
+
128
+ let timer: NodeJS.Timeout | undefined;
129
+ let resolveTimeout: ((value: { kind: "timeout" }) => void) | undefined;
130
+ const enforceTimeout = !noTimeoutEnforced && activeTimeoutMs !== undefined && activeTimeoutMs > 0;
131
+ if (enforceTimeout) {
132
+ const { promise, resolve } = Promise.withResolvers<{ kind: "timeout" }>();
133
+ resolveTimeout = resolve;
134
+ timer = setTimeout(() => resolve({ kind: "timeout" }), activeTimeoutMs);
135
+ racers.push(promise);
116
136
  }
117
137
 
118
- const { promise: timeoutPromise, resolve: resolveTimeout } = Promise.withResolvers<{
119
- kind: "timeout";
120
- }>();
121
- const timer = setTimeout(() => resolveTimeout({ kind: "timeout" }), activeTimeoutMs);
138
+ let abortListener: (() => void) | undefined;
139
+ let resolveAbort: ((value: { kind: "abort" }) => void) | undefined;
140
+ if (abortSignal) {
141
+ const { promise, resolve } = Promise.withResolvers<{ kind: "abort" }>();
142
+ resolveAbort = resolve;
143
+ abortListener = () => resolve({ kind: "abort" });
144
+ abortSignal.addEventListener("abort", abortListener, { once: true });
145
+ racers.push(promise);
146
+ }
122
147
 
123
148
  try {
124
- const outcome = await Promise.race([nextResultPromise, timeoutPromise]);
149
+ const outcome = await Promise.race(racers);
150
+ if (outcome.kind === "abort") {
151
+ closeIterator();
152
+ throw abortReason(abortSignal!);
153
+ }
125
154
  if (outcome.kind === "timeout") {
126
155
  if (!onFirst) {
127
156
  options.onIdle?.();
128
157
  } else {
129
158
  options.onFirstItemTimeout?.();
130
159
  }
131
- const returnPromise = iterator.return?.();
132
- if (returnPromise) {
133
- void returnPromise.catch(() => {});
134
- }
160
+ closeIterator();
135
161
  throw new Error(!onFirst ? options.errorMessage : (options.firstItemErrorMessage ?? options.errorMessage));
136
162
  }
137
- watchdog && clearTimeout(watchdog);
138
- watchdog = undefined;
139
163
  if (outcome.kind === "error") {
140
164
  throw outcome.error;
141
165
  }
166
+ watchdog && clearTimeout(watchdog);
167
+ watchdog = undefined;
142
168
  if (outcome.result.done) {
143
169
  return;
144
170
  }
145
171
  onFirst?.();
146
172
  yield outcome.result.value;
147
173
  } finally {
148
- clearTimeout(timer);
174
+ if (timer !== undefined) clearTimeout(timer);
175
+ // Resolve dangling promises so the racers don't leak (Promise.race is one-shot).
176
+ resolveTimeout?.({ kind: "timeout" });
177
+ if (abortListener && abortSignal) {
178
+ abortSignal.removeEventListener("abort", abortListener);
179
+ }
180
+ resolveAbort?.({ kind: "abort" });
149
181
  }
150
182
  }
151
183
  }
184
+
185
+ function abortReason(signal: AbortSignal): Error {
186
+ const reason = signal.reason;
187
+ if (reason instanceof Error) return reason;
188
+ if (typeof reason === "string") return new Error(reason);
189
+ return new Error("Request was aborted");
190
+ }