@oh-my-pi/pi-ai 15.8.0 → 15.8.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.8.2] - 2026-06-03
6
+
7
+ ### Fixed
8
+
9
+ - Fixed `opencode-zen/minimax-m3-free` (and forward-compat `opencode-zen/minimax-m3`) and `opencode-go/minimax-m3` being routed to `anthropic-messages` despite the OpenCode Zen/Go gateways only serving these ids at `/v1/chat/completions`, which surfaced raw MiniMax/tool-call markup (`<invoke name="bash">`, `<tool_call>`, `<description>`, `<cwd>`, `<|minimax|>`) in the UI. Resolver overrides now pin these ids to `openai-completions` and the bundled `models.json` entries are flipped to match. ([#1617](https://github.com/can1357/oh-my-pi/issues/1617))
10
+ - Fixed MiniMax Coding Plan China login opening the international `platform.minimax.io` subscription page instead of the China `platform.minimaxi.com` page.
11
+
5
12
  ## [15.8.0] - 2026-06-02
6
13
  ### Added
7
14
 
@@ -2911,4 +2918,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
2911
2918
 
2912
2919
  ## [0.9.4] - 2025-11-26
2913
2920
 
2914
- Initial release with multi-provider LLM support.
2921
+ Initial release with multi-provider LLM support.
@@ -5,7 +5,7 @@
5
5
  * MiniMax models (M2, M2.1) through an OpenAI-compatible API.
6
6
  *
7
7
  * This is not OAuth - it's a simple API key flow:
8
- * 1. Open browser to https://platform.minimax.io/subscribe/coding-plan
8
+ * 1. Open browser to the matching regional MiniMax subscription page
9
9
  * 2. User subscribes and copies their API key
10
10
  * 3. User pastes the API key back into the CLI
11
11
  *
@@ -47,8 +47,7 @@ export declare function zodToWireSchema(schema: ZodType): Record<string, unknown
47
47
  * over the wire. Zod schemas are converted (and cached); legacy TypeBox / raw
48
48
  * JSON Schema parameters are upgraded to draft 2020-12 (and cached).
49
49
  *
50
- * Both branches finish with `normalizeEmptySchemas` so every provider
51
- * OpenAI, Anthropic, Google, Ollama, Bedrock, Cursor — sees `{}` normalized
52
- * to `true` in schema-valued positions (issue #1179).
50
+ * Zod schemas also receive Zod-artifact cleanup; both branches normalize
51
+ * schema-valued positions and nullable scalar unions.
53
52
  */
54
53
  export declare function toolWireSchema(tool: Tool): Record<string, unknown>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "15.8.0",
4
+ "version": "15.8.3",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@bufbuild/protobuf": "^2.12.0",
42
- "@oh-my-pi/pi-utils": "15.8.0",
42
+ "@oh-my-pi/pi-utils": "15.8.3",
43
43
  "openai": "^6.39.0",
44
44
  "partial-json": "^0.1.7",
45
45
  "zod": "4.4.3"
package/src/models.json CHANGED
@@ -61520,9 +61520,9 @@
61520
61520
  "minimax-m3": {
61521
61521
  "id": "minimax-m3",
61522
61522
  "name": "MiniMax M3",
61523
- "api": "anthropic-messages",
61523
+ "api": "openai-completions",
61524
61524
  "provider": "opencode-go",
61525
- "baseUrl": "https://opencode.ai/zen/go",
61525
+ "baseUrl": "https://opencode.ai/zen/go/v1",
61526
61526
  "reasoning": true,
61527
61527
  "input": [
61528
61528
  "text",
@@ -62924,9 +62924,9 @@
62924
62924
  "minimax-m3-free": {
62925
62925
  "id": "minimax-m3-free",
62926
62926
  "name": "MiniMax M3 Free",
62927
- "api": "anthropic-messages",
62927
+ "api": "openai-completions",
62928
62928
  "provider": "opencode-zen",
62929
- "baseUrl": "https://opencode.ai/zen",
62929
+ "baseUrl": "https://opencode.ai/zen/v1",
62930
62930
  "reasoning": true,
62931
62931
  "input": [
62932
62932
  "text",
@@ -2471,19 +2471,32 @@ function createOpenCodeApiResolution(
2471
2471
  };
2472
2472
  }
2473
2473
 
2474
- const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen");
2474
+ // OpenCode Zen: models.dev declares minimax-m3-free (and forward-compat
2475
+ // minimax-m3) with `provider.npm = "@ai-sdk/anthropic"`, but the Zen gateway
2476
+ // only serves them at https://opencode.ai/zen/v1/chat/completions (verified
2477
+ // against the live /v1/models response — minimax-m3-free is listed there, and
2478
+ // the gateway has no /v1/messages route for it). Without this override the
2479
+ // resolver POSTs anthropic-shaped requests to /v1/messages and the UI surfaces
2480
+ // raw <invoke>/<|minimax|>/<tool_call> markup (#1617).
2481
+ const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen", {
2482
+ "minimax-m3": "openai-completions",
2483
+ "minimax-m3-free": "openai-completions",
2484
+ });
2475
2485
  // OpenCode Go: models.dev declares minimax-m2.7 / qwen3.5-plus / qwen3.6-plus
2476
- // with `provider.npm = "@ai-sdk/anthropic"`, but the OpenCode Go gateway only
2477
- // serves them at `https://opencode.ai/zen/go/v1/chat/completions` (verified
2478
- // against https://opencode.ai/zen/go/v1/models and the upstream endpoint
2479
- // table at https://opencode.ai/docs/go/#endpoints minimax-m2.5 works the
2480
- // same way and lacks an `npm` field on models.dev so it already falls through
2481
- // to the openai-completions default). Without this override the resolver
2482
- // would POST anthropic-style requests to /v1/messages and the gateway would
2483
- // return its `Page Not Found` HTML (issue #887). Override the resolver so
2484
- // regenerating models.json keeps the correct routing.
2486
+ // (and now also minimax-m3) with `provider.npm = "@ai-sdk/anthropic"`, but
2487
+ // the OpenCode Go gateway only serves them at
2488
+ // `https://opencode.ai/zen/go/v1/chat/completions` (verified against
2489
+ // https://opencode.ai/zen/go/v1/models and the upstream endpoint table at
2490
+ // https://opencode.ai/docs/go/#endpoints minimax-m2.5 works the same way
2491
+ // and lacks an `npm` field on models.dev so it already falls through to the
2492
+ // openai-completions default). Without this override the resolver would POST
2493
+ // anthropic-style requests to /v1/messages and the gateway would return its
2494
+ // `Page Not Found` HTML (issue #887 for the qwen/m2.7 entries; minimax-m3
2495
+ // and minimax-m3-free added under #1617 for the same root cause).
2485
2496
  const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen/go", {
2486
2497
  "minimax-m2.7": "openai-completions",
2498
+ "minimax-m3": "openai-completions",
2499
+ "minimax-m3-free": "openai-completions",
2487
2500
  "qwen3.5-plus": "openai-completions",
2488
2501
  "qwen3.6-plus": "openai-completions",
2489
2502
  });
@@ -1933,9 +1933,26 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
1933
1933
  };
1934
1934
  }
1935
1935
 
1936
- // OpenCode's Anthropic-compatible gateway accepts bearer auth only; leaving
1937
- // apiKey set lets the SDK add X-Api-Key, which upstream Alibaba rejects.
1938
- if (model.provider === "opencode-go" || model.provider === "opencode-zen") {
1936
+ // OpenCode Go's Anthropic-compatible gateway validates API-key auth through
1937
+ // `X-Api-Key`; bearer-only requests reach the endpoint but return
1938
+ // `Missing API key` before token validation.
1939
+ if (model.provider === "opencode-go") {
1940
+ delete defaultHeaders.Authorization;
1941
+ return {
1942
+ isOAuthToken: false,
1943
+ apiKey,
1944
+ authToken: null,
1945
+ baseURL: baseUrl,
1946
+ maxRetries: 5,
1947
+ defaultHeaders,
1948
+ ...(debugFetch ? { fetch: debugFetch } : {}),
1949
+ ...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
1950
+ };
1951
+ }
1952
+
1953
+ // OpenCode Zen's Anthropic-compatible gateway accepts bearer auth only;
1954
+ // leaving apiKey set lets the client add X-Api-Key, which upstream Alibaba rejects.
1955
+ if (model.provider === "opencode-zen") {
1939
1956
  return {
1940
1957
  isOAuthToken: false,
1941
1958
  apiKey: null,
@@ -5,7 +5,7 @@
5
5
  * MiniMax models (M2, M2.1) through an OpenAI-compatible API.
6
6
  *
7
7
  * This is not OAuth - it's a simple API key flow:
8
- * 1. Open browser to https://platform.minimax.io/subscribe/coding-plan
8
+ * 1. Open browser to the matching regional MiniMax subscription page
9
9
  * 2. User subscribes and copies their API key
10
10
  * 3. User pastes the API key back into the CLI
11
11
  *
@@ -16,7 +16,8 @@
16
16
  import { validateOpenAICompatibleApiKey } from "./api-key-validation";
17
17
  import type { OAuthController } from "./types";
18
18
 
19
- const AUTH_URL = "https://platform.minimax.io/subscribe/coding-plan";
19
+ const AUTH_URL_INTL = "https://platform.minimax.io/subscribe/coding-plan";
20
+ const AUTH_URL_CN = "https://platform.minimaxi.com/subscribe/coding-plan";
20
21
  const API_BASE_URL_INTL = "https://api.minimax.io/v1";
21
22
  const API_BASE_URL_CN = "https://api.minimaxi.com/v1";
22
23
  const VALIDATION_MODEL = "MiniMax-M2";
@@ -28,11 +29,12 @@ const VALIDATION_MODEL = "MiniMax-M2";
28
29
  * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
29
30
  */
30
31
  export async function loginMiniMaxCode(options: OAuthController): Promise<string> {
31
- return loginMiniMaxCodeWithBaseUrl(options, API_BASE_URL_INTL, "MiniMax Coding Plan");
32
+ return loginMiniMaxCodeWithBaseUrl(options, AUTH_URL_INTL, API_BASE_URL_INTL, "MiniMax Coding Plan");
32
33
  }
33
34
 
34
35
  async function loginMiniMaxCodeWithBaseUrl(
35
36
  options: OAuthController,
37
+ authUrl: string,
36
38
  baseUrl: string,
37
39
  providerName: string,
38
40
  ): Promise<string> {
@@ -41,7 +43,7 @@ async function loginMiniMaxCodeWithBaseUrl(
41
43
  }
42
44
  // Open browser to subscription page
43
45
  options.onAuth?.({
44
- url: AUTH_URL,
46
+ url: authUrl,
45
47
  instructions: "Subscribe to Coding Plan and copy your API key",
46
48
  });
47
49
  // Prompt user to paste their API key
@@ -74,5 +76,5 @@ async function loginMiniMaxCodeWithBaseUrl(
74
76
  * Same flow as international but uses China endpoint.
75
77
  */
76
78
  export async function loginMiniMaxCodeCn(options: OAuthController): Promise<string> {
77
- return loginMiniMaxCodeWithBaseUrl(options, API_BASE_URL_CN, "MiniMax Coding Plan (China)");
79
+ return loginMiniMaxCodeWithBaseUrl(options, AUTH_URL_CN, API_BASE_URL_CN, "MiniMax Coding Plan (China)");
78
80
  }
@@ -68,13 +68,93 @@ const kJsonWireSchema = Symbol("pi.schema.json.wire");
68
68
  */
69
69
  function postProcess(schema: Record<string, unknown>): Record<string, unknown> {
70
70
  delete schema.$schema;
71
- walk(schema);
71
+ walk(schema, true);
72
+ normalizeEmptySchemas(schema);
73
+ return schema;
74
+ }
75
+
76
+ function postProcessJsonSchema(schema: Record<string, unknown>): Record<string, unknown> {
77
+ walk(schema, false);
72
78
  normalizeEmptySchemas(schema);
73
79
  return schema;
74
80
  }
75
81
 
76
82
  const SAFE_INTEGER_MAX = Number.MAX_SAFE_INTEGER;
77
83
  const SAFE_INTEGER_MIN = Number.MIN_SAFE_INTEGER;
84
+ const NULLABLE_SCALAR_TYPES = new Set(["string", "number", "integer", "boolean"]);
85
+
86
+ const SCHEMA_DEFINING_SIBLING_KEYS = new Set([
87
+ "$ref",
88
+ "additionalProperties",
89
+ "allOf",
90
+ "anyOf",
91
+ "const",
92
+ "contains",
93
+ "enum",
94
+ "if",
95
+ "items",
96
+ "not",
97
+ "oneOf",
98
+ "patternProperties",
99
+ "prefixItems",
100
+ "properties",
101
+ "propertyNames",
102
+ "then",
103
+ "else",
104
+ "unevaluatedItems",
105
+ "unevaluatedProperties",
106
+ ]);
107
+
108
+ function isSchemaRecord(value: unknown): value is Record<string, unknown> {
109
+ return value !== null && typeof value === "object" && !Array.isArray(value);
110
+ }
111
+
112
+ function hasSchemaDefiningSibling(schema: Record<string, unknown>): boolean {
113
+ for (const key in schema) {
114
+ if (key !== "anyOf" && SCHEMA_DEFINING_SIBLING_KEYS.has(key)) return true;
115
+ }
116
+ return false;
117
+ }
118
+
119
+ function isNullVariant(schema: Record<string, unknown>): boolean {
120
+ return schema.type === "null" && Object.keys(schema).length === 1;
121
+ }
122
+
123
+ function isScalarVariant(schema: Record<string, unknown>): schema is Record<string, unknown> & { type: string } {
124
+ return typeof schema.type === "string" && NULLABLE_SCALAR_TYPES.has(schema.type);
125
+ }
126
+
127
+ function hasIntegerType(type: unknown): boolean {
128
+ return type === "integer" || (Array.isArray(type) && type.includes("integer"));
129
+ }
130
+
131
+ function rewriteNullableScalarAnyOf(schema: Record<string, unknown>): void {
132
+ if (hasSchemaDefiningSibling(schema)) return;
133
+ const variants = schema.anyOf;
134
+ if (!Array.isArray(variants) || variants.length !== 2) return;
135
+
136
+ let scalarVariant: Record<string, unknown> | undefined;
137
+ let scalarType: string | undefined;
138
+ let sawNull = false;
139
+ for (const variant of variants) {
140
+ if (!isSchemaRecord(variant)) return;
141
+ if (isNullVariant(variant)) {
142
+ if (sawNull) return;
143
+ sawNull = true;
144
+ continue;
145
+ }
146
+ if (!isScalarVariant(variant) || scalarVariant) return;
147
+ scalarVariant = variant;
148
+ scalarType = variant.type;
149
+ }
150
+ if (!sawNull || !scalarVariant || !scalarType) return;
151
+
152
+ delete schema.anyOf;
153
+ for (const key in scalarVariant) {
154
+ if (key !== "type" && !Object.hasOwn(schema, key)) schema[key] = scalarVariant[key];
155
+ }
156
+ schema.type = [scalarType, "null"];
157
+ }
78
158
 
79
159
  /** Keys whose values are a single JSON Schema (not an array or map). */
80
160
  const SCHEMA_VALUE_KEYS = [
@@ -102,39 +182,42 @@ function isEmptyObject(val: unknown): val is Record<string, never> {
102
182
  return Object.keys(val).length === 0;
103
183
  }
104
184
 
105
- function walk(node: unknown): void {
185
+ function walk(node: unknown, zodCleanup: boolean): void {
106
186
  if (Array.isArray(node)) {
107
- for (const child of node) walk(child);
187
+ for (const child of node) walk(child, zodCleanup);
108
188
  return;
109
189
  }
110
190
  if (!node || typeof node !== "object") return;
111
191
  const obj = node as Record<string, unknown>;
192
+ rewriteNullableScalarAnyOf(obj);
112
193
 
113
- // Drop noise injected for `z.number().int()`.
114
- if (obj.type === "integer") {
115
- if (obj.minimum === SAFE_INTEGER_MIN) delete obj.minimum;
116
- if (obj.maximum === SAFE_INTEGER_MAX) delete obj.maximum;
117
- }
194
+ if (zodCleanup) {
195
+ // Drop noise injected for `z.number().int()`.
196
+ if (hasIntegerType(obj.type)) {
197
+ if (obj.minimum === SAFE_INTEGER_MIN) delete obj.minimum;
198
+ if (obj.maximum === SAFE_INTEGER_MAX) delete obj.maximum;
199
+ }
118
200
 
119
- // Make defaulted properties non-required.
120
- if (Array.isArray(obj.required) && obj.properties && typeof obj.properties === "object") {
121
- const properties = obj.properties as Record<string, unknown>;
122
- const required = obj.required as string[];
123
- const filtered = required.filter(name => {
124
- const propertySchema = properties[name];
125
- if (!propertySchema || typeof propertySchema !== "object") return true;
126
- return !("default" in (propertySchema as Record<string, unknown>));
127
- });
128
- if (filtered.length !== required.length) {
129
- if (filtered.length === 0) {
130
- delete obj.required;
131
- } else {
132
- obj.required = filtered;
201
+ // Make defaulted properties non-required.
202
+ if (Array.isArray(obj.required) && obj.properties && typeof obj.properties === "object") {
203
+ const properties = obj.properties as Record<string, unknown>;
204
+ const required = obj.required as string[];
205
+ const filtered = required.filter(name => {
206
+ const propertySchema = properties[name];
207
+ if (!propertySchema || typeof propertySchema !== "object") return true;
208
+ return !("default" in (propertySchema as Record<string, unknown>));
209
+ });
210
+ if (filtered.length !== required.length) {
211
+ if (filtered.length === 0) {
212
+ delete obj.required;
213
+ } else {
214
+ obj.required = filtered;
215
+ }
133
216
  }
134
217
  }
135
218
  }
136
219
 
137
- for (const k in obj) walk(obj[k]);
220
+ for (const k in obj) walk(obj[k], zodCleanup);
138
221
  }
139
222
 
140
223
  /**
@@ -197,16 +280,14 @@ export function zodToWireSchema(schema: ZodType): Record<string, unknown> {
197
280
  * over the wire. Zod schemas are converted (and cached); legacy TypeBox / raw
198
281
  * JSON Schema parameters are upgraded to draft 2020-12 (and cached).
199
282
  *
200
- * Both branches finish with `normalizeEmptySchemas` so every provider
201
- * OpenAI, Anthropic, Google, Ollama, Bedrock, Cursor — sees `{}` normalized
202
- * to `true` in schema-valued positions (issue #1179).
283
+ * Zod schemas also receive Zod-artifact cleanup; both branches normalize
284
+ * schema-valued positions and nullable scalar unions.
203
285
  */
204
286
  export function toolWireSchema(tool: Tool): Record<string, unknown> {
205
287
  const params: TSchema = tool.parameters;
206
288
  if (isZodSchema(params)) return zodToWireSchema(params);
207
289
  return stamp(params as Record<string, unknown>, kJsonWireSchema, p => {
208
290
  const upgraded = upgradeJsonSchemaTo202012(p) as Record<string, unknown>;
209
- normalizeEmptySchemas(upgraded);
210
- return upgraded;
291
+ return postProcessJsonSchema(upgraded);
211
292
  });
212
293
  }