@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 +8 -1
- package/dist/types/utils/oauth/minimax-code.d.ts +1 -1
- package/dist/types/utils/schema/wire.d.ts +2 -3
- package/package.json +2 -2
- package/src/models.json +4 -4
- package/src/provider-models/openai-compat.ts +23 -10
- package/src/providers/anthropic.ts +20 -3
- package/src/utils/oauth/minimax-code.ts +7 -5
- package/src/utils/schema/wire.ts +109 -28
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
|
|
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
|
-
*
|
|
51
|
-
*
|
|
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.
|
|
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.
|
|
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": "
|
|
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": "
|
|
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
|
-
|
|
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
|
|
2477
|
-
// serves them at
|
|
2478
|
-
//
|
|
2479
|
-
//
|
|
2480
|
-
//
|
|
2481
|
-
//
|
|
2482
|
-
//
|
|
2483
|
-
//
|
|
2484
|
-
//
|
|
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
|
|
1937
|
-
//
|
|
1938
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
}
|
package/src/utils/schema/wire.ts
CHANGED
|
@@ -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
|
-
|
|
114
|
-
|
|
115
|
-
if (obj.
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
*
|
|
201
|
-
*
|
|
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
|
-
|
|
210
|
-
return upgraded;
|
|
291
|
+
return postProcessJsonSchema(upgraded);
|
|
211
292
|
});
|
|
212
293
|
}
|