@gajae-code/ai 0.17.1 → 0.17.2
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 +14 -0
- package/dist/types/model-thinking.d.ts +10 -6
- package/dist/types/provider-models/openai-compat.d.ts +2 -2
- package/dist/types/utils/h2-fetch.d.ts +6 -1
- package/package.json +3 -3
- package/src/model-manager.ts +11 -8
- package/src/model-thinking.d.ts +10 -6
- package/src/model-thinking.ts +53 -7
- package/src/models.json +50 -0
- package/src/provider-models/openai-compat.ts +27 -19
- package/src/providers/anthropic.ts +9 -1
- package/src/providers/cursor.ts +32 -7
- package/src/providers/openai-completions.ts +8 -2
- package/src/providers/openai-opencodex-responses.ts +15 -5
- package/src/stream.ts +17 -1
- package/src/utils/h2-fetch.ts +26 -2
- package/src/utils/http-inspector.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.17.2] - 2026-09-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Union Alpha Free on OpenCode Go and Zen with Anthropic Messages routing, image input, reasoning, and the published free-tier limits.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Share Cursor HTTP/2 write error and close listeners across pending frames to avoid listener-limit warnings during write bursts while preserving write-failure and drain-timeout handling.
|
|
14
|
+
|
|
15
|
+
- Discover local OpenCodex models through the public `/v1/models` endpoint instead of the admin-only management API, preserving public context, input, and reasoning capabilities.
|
|
16
|
+
|
|
17
|
+
- Preserve OpenCode protocol-specific base URLs during model discovery and recover reviewed Union Alpha limits from pre-catalogue discovery caches.
|
|
18
|
+
|
|
5
19
|
## [0.17.1] - 2026-09-17
|
|
6
20
|
|
|
7
21
|
## [0.17.0] - 2026-09-17
|
|
@@ -28,14 +28,18 @@ export declare function enrichModelThinking<TApi extends Api>(model: ApiModel<TA
|
|
|
28
28
|
* canonical rules, replacing any existing `thinking`.
|
|
29
29
|
*/
|
|
30
30
|
export declare function refreshModelThinking<TApi extends Api>(model: ApiModel<TApi>): ApiModel<TApi>;
|
|
31
|
+
/**
|
|
32
|
+
* Native MiniMax thinking semantics, scoped to first-party regional routes.
|
|
33
|
+
* M3 supports adaptive/disabled; M2.x always thinks, even when disabled is sent.
|
|
34
|
+
* https://platform.minimax.io/docs/api-reference/text-openai-api#thinking-control
|
|
35
|
+
* https://platform.minimax.io/docs/api-reference/text-anthropic-api#thinking-control
|
|
36
|
+
*/
|
|
37
|
+
export declare function getMiniMaxThinkingMode(model: ApiModel<Api>, resolvedBaseUrl?: string): "toggle" | "always-on" | undefined;
|
|
31
38
|
/**
|
|
32
39
|
* Returns whether the configured transport has an audited user-facing reasoning control.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* endpoints must opt in with `compat.supportsReasoningEffort: true`; providers using a
|
|
37
|
-
* non-OpenAI request shape must also declare `compat.thinkingFormat`. Bundled providers
|
|
38
|
-
* remain governed by their catalog and compatibility metadata.
|
|
40
|
+
* Custom OpenAI-compatible endpoints must opt in with supportsReasoningEffort and,
|
|
41
|
+
* for non-OpenAI request shapes, thinkingFormat. Native MiniMax switches are
|
|
42
|
+
* separate from reasoning_effort, which those endpoints do not support.
|
|
39
43
|
*/
|
|
40
44
|
export declare function modelSupportsReasoningControl<TApi extends Api>(model: ApiModel<TApi>, resolvedBaseUrl?: string): boolean;
|
|
41
45
|
/**
|
|
@@ -97,8 +97,8 @@ export interface OpenCodeModelManagerConfig {
|
|
|
97
97
|
apiKey?: string;
|
|
98
98
|
baseUrl?: string;
|
|
99
99
|
}
|
|
100
|
-
export declare function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<
|
|
101
|
-
export declare function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<
|
|
100
|
+
export declare function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api>;
|
|
101
|
+
export declare function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api>;
|
|
102
102
|
export declare function commandCodeModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api>;
|
|
103
103
|
export interface OllamaModelManagerConfig {
|
|
104
104
|
apiKey?: string;
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
* advertise h2 via ALPN but then refuse or reset the connection at the HTTP/2
|
|
13
13
|
* framing layer. Bun surfaces these as `ConnectionRefused`, `ConnectionReset`,
|
|
14
14
|
* or `ConnectionClosed` rather than `HTTP2Unsupported`, so we treat those
|
|
15
|
-
* codes as h2-fallback triggers as well.
|
|
15
|
+
* codes as h2-fallback triggers as well. `ConnectionRefused` is raised before
|
|
16
|
+
* the request is written, but a reset or a close does not prove the peer never
|
|
17
|
+
* consumed the body — it may have processed the request and died before
|
|
18
|
+
* answering. Replaying those two on h1 would duplicate the side effect, so
|
|
19
|
+
* `ConnectionReset` and `ConnectionClosed` fall back only for replay-safe
|
|
20
|
+
* methods; anything else rethrows the original error.
|
|
16
21
|
*
|
|
17
22
|
* ALPN-refusing hosts (notably zcode.z.ai, the GLM ZCode OAuth broker) abort
|
|
18
23
|
* the TLS handshake entirely when the client offers ALPN h2. Bun reports that
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.17.
|
|
4
|
+
"version": "0.17.2",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
42
42
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
43
43
|
"@bufbuild/protobuf": "^2.12.0",
|
|
44
|
-
"@gajae-code/natives": "0.17.
|
|
45
|
-
"@gajae-code/utils": "0.17.
|
|
44
|
+
"@gajae-code/natives": "0.17.2",
|
|
45
|
+
"@gajae-code/utils": "0.17.2",
|
|
46
46
|
"openai": "^6.36.0",
|
|
47
47
|
"partial-json": "^0.1.7",
|
|
48
48
|
"zod": "4.4.3"
|
package/src/model-manager.ts
CHANGED
|
@@ -553,15 +553,18 @@ function fingerprintStatic<TApi extends Api>(models: readonly Model<TApi>[]): st
|
|
|
553
553
|
|
|
554
554
|
function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamicModel: Model<TApi>): Model<TApi> {
|
|
555
555
|
const supportsImage = existingModel.input.includes("image") || dynamicModel.input.includes("image");
|
|
556
|
-
// Before
|
|
556
|
+
// Before these exact OpenCode models were curated, ID-only discovery cached the
|
|
557
557
|
// non-reasoning Completions defaults. A fresh authoritative cache can skip
|
|
558
558
|
// discovery after an upgrade, so recover its limits from reviewed static
|
|
559
|
-
//
|
|
559
|
+
// metadata here. Do not reinterpret individual numeric limits or
|
|
560
560
|
// apply this correction to reviewed discovery rows or other model IDs.
|
|
561
|
-
const
|
|
562
|
-
existingModel.provider === "opencode-go" &&
|
|
563
|
-
|
|
564
|
-
|
|
561
|
+
const hasPreReviewOpenCodeLimits =
|
|
562
|
+
((existingModel.provider === "opencode-go" &&
|
|
563
|
+
existingModel.id === "muse-spark-1.3-contributor" &&
|
|
564
|
+
existingModel.api === "openai-responses") ||
|
|
565
|
+
((existingModel.provider === "opencode-go" || existingModel.provider === "opencode-zen") &&
|
|
566
|
+
existingModel.id === "union-alpha" &&
|
|
567
|
+
existingModel.api === "anthropic-messages")) &&
|
|
565
568
|
existingModel.reasoning &&
|
|
566
569
|
dynamicModel.api === "openai-completions" &&
|
|
567
570
|
!dynamicModel.reasoning &&
|
|
@@ -594,10 +597,10 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
|
|
|
594
597
|
cacheRead: preferDiscoveryCost(dynamicModel.cost.cacheRead, existingModel.cost.cacheRead),
|
|
595
598
|
cacheWrite: preferDiscoveryCost(dynamicModel.cost.cacheWrite, existingModel.cost.cacheWrite),
|
|
596
599
|
},
|
|
597
|
-
contextWindow:
|
|
600
|
+
contextWindow: hasPreReviewOpenCodeLimits
|
|
598
601
|
? existingModel.contextWindow
|
|
599
602
|
: preferDiscoveryLimit(dynamicModel.contextWindow, existingModel.contextWindow),
|
|
600
|
-
maxTokens:
|
|
603
|
+
maxTokens: hasPreReviewOpenCodeLimits
|
|
601
604
|
? existingModel.maxTokens
|
|
602
605
|
: preferDiscoveryLimit(dynamicModel.maxTokens, existingModel.maxTokens),
|
|
603
606
|
headers: dynamicModel.headers ? { ...existingModel.headers, ...dynamicModel.headers } : existingModel.headers,
|
package/src/model-thinking.d.ts
CHANGED
|
@@ -28,14 +28,18 @@ export declare function enrichModelThinking<TApi extends Api>(model: ApiModel<TA
|
|
|
28
28
|
* canonical rules, replacing any existing `thinking`.
|
|
29
29
|
*/
|
|
30
30
|
export declare function refreshModelThinking<TApi extends Api>(model: ApiModel<TApi>): ApiModel<TApi>;
|
|
31
|
+
/**
|
|
32
|
+
* Native MiniMax thinking semantics, scoped to first-party regional routes.
|
|
33
|
+
* M3 supports adaptive/disabled; M2.x always thinks, even when disabled is sent.
|
|
34
|
+
* https://platform.minimax.io/docs/api-reference/text-openai-api#thinking-control
|
|
35
|
+
* https://platform.minimax.io/docs/api-reference/text-anthropic-api#thinking-control
|
|
36
|
+
*/
|
|
37
|
+
export declare function getMiniMaxThinkingMode(model: ApiModel<Api>, resolvedBaseUrl?: string): "toggle" | "always-on" | undefined;
|
|
31
38
|
/**
|
|
32
39
|
* Returns whether the configured transport has an audited user-facing reasoning control.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* endpoints must opt in with `compat.supportsReasoningEffort: true`; providers using a
|
|
37
|
-
* non-OpenAI request shape must also declare `compat.thinkingFormat`. Bundled providers
|
|
38
|
-
* remain governed by their catalog and compatibility metadata.
|
|
40
|
+
* Custom OpenAI-compatible endpoints must opt in with supportsReasoningEffort and,
|
|
41
|
+
* for non-OpenAI request shapes, thinkingFormat. Native MiniMax switches are
|
|
42
|
+
* separate from reasoning_effort, which those endpoints do not support.
|
|
39
43
|
*/
|
|
40
44
|
export declare function modelSupportsReasoningControl<TApi extends Api>(model: ApiModel<TApi>, resolvedBaseUrl?: string): boolean;
|
|
41
45
|
/**
|
package/src/model-thinking.ts
CHANGED
|
@@ -181,7 +181,7 @@ export function enrichModelThinking<TApi extends Api>(model: ApiModel<TApi>): Ap
|
|
|
181
181
|
if (cached !== undefined) {
|
|
182
182
|
return cached as ApiModel<TApi>;
|
|
183
183
|
}
|
|
184
|
-
const normalizedThinking = normalizeThinkingConfig(model.thinking);
|
|
184
|
+
const normalizedThinking = getMiniMaxThinkingMode(model) ? undefined : normalizeThinkingConfig(model.thinking);
|
|
185
185
|
let result: ApiModel<TApi>;
|
|
186
186
|
if (isGroqCompoundReasoningUnsupported(model)) {
|
|
187
187
|
result =
|
|
@@ -228,20 +228,61 @@ export function refreshModelThinking<TApi extends Api>(model: ApiModel<TApi>): A
|
|
|
228
228
|
return { ...model, thinking: inferModelThinking(model) };
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Native MiniMax thinking semantics, scoped to first-party regional routes.
|
|
233
|
+
* M3 supports adaptive/disabled; M2.x always thinks, even when disabled is sent.
|
|
234
|
+
* https://platform.minimax.io/docs/api-reference/text-openai-api#thinking-control
|
|
235
|
+
* https://platform.minimax.io/docs/api-reference/text-anthropic-api#thinking-control
|
|
236
|
+
*/
|
|
237
|
+
export function getMiniMaxThinkingMode(
|
|
238
|
+
model: ApiModel<Api>,
|
|
239
|
+
resolvedBaseUrl?: string,
|
|
240
|
+
): "toggle" | "always-on" | undefined {
|
|
241
|
+
const isDirectRoute =
|
|
242
|
+
(model.api === "anthropic-messages" && (model.provider === "minimax" || model.provider === "minimax-cn")) ||
|
|
243
|
+
(model.api === "openai-completions" &&
|
|
244
|
+
(model.provider === "minimax-code" || model.provider === "minimax-code-cn"));
|
|
245
|
+
if (!isDirectRoute) return undefined;
|
|
246
|
+
// Provider identity survives baseUrl overrides. Only the normalized native
|
|
247
|
+
// endpoint, not a provider label or a matching hostname suffix, proves this contract.
|
|
248
|
+
try {
|
|
249
|
+
const endpoint = new URL(resolvedBaseUrl ?? model.baseUrl);
|
|
250
|
+
const host = model.provider.endsWith("-cn") ? "api.minimaxi.com" : "api.minimax.io";
|
|
251
|
+
const path = endpoint.pathname.replace(/\/+$/, "");
|
|
252
|
+
const validPath =
|
|
253
|
+
model.api === "anthropic-messages" ? path === "/anthropic" || path === "/anthropic/v1" : path === "/v1";
|
|
254
|
+
if (
|
|
255
|
+
endpoint.origin !== `https://${host}` ||
|
|
256
|
+
!validPath ||
|
|
257
|
+
endpoint.username ||
|
|
258
|
+
endpoint.password ||
|
|
259
|
+
endpoint.search ||
|
|
260
|
+
endpoint.hash
|
|
261
|
+
)
|
|
262
|
+
return undefined;
|
|
263
|
+
} catch {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
if (model.id === "MiniMax-M3" || model.id === "MiniMax-M3[1m]") return "toggle";
|
|
267
|
+
if (/^MiniMax-M2(?:[.\-[]|$)/.test(model.id)) return "always-on";
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
231
271
|
/**
|
|
232
272
|
* Returns whether the configured transport has an audited user-facing reasoning control.
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* endpoints must opt in with `compat.supportsReasoningEffort: true`; providers using a
|
|
237
|
-
* non-OpenAI request shape must also declare `compat.thinkingFormat`. Bundled providers
|
|
238
|
-
* remain governed by their catalog and compatibility metadata.
|
|
273
|
+
* Custom OpenAI-compatible endpoints must opt in with supportsReasoningEffort and,
|
|
274
|
+
* for non-OpenAI request shapes, thinkingFormat. Native MiniMax switches are
|
|
275
|
+
* separate from reasoning_effort, which those endpoints do not support.
|
|
239
276
|
*/
|
|
240
277
|
export function modelSupportsReasoningControl<TApi extends Api>(
|
|
241
278
|
model: ApiModel<TApi>,
|
|
242
279
|
resolvedBaseUrl?: string,
|
|
243
280
|
): boolean {
|
|
244
281
|
if (!model.reasoning) return false;
|
|
282
|
+
// MiniMax's native thinking switch is not OpenAI reasoning_effort. M2.x
|
|
283
|
+
// always thinks; M3 supports adaptive/disabled, but no effort or budget.
|
|
284
|
+
const miniMaxMode = getMiniMaxThinkingMode(model, resolvedBaseUrl);
|
|
285
|
+
if (miniMaxMode) return miniMaxMode === "toggle";
|
|
245
286
|
if (model.api === "openai-completions") {
|
|
246
287
|
const completionsModel = model as ApiModel<"openai-completions">;
|
|
247
288
|
const explicitSupport = completionsModel.compat?.supportsReasoningEffort;
|
|
@@ -777,6 +818,11 @@ function inferDefaultEffort<TApi extends Api>(model: ApiModel<TApi>, parsedModel
|
|
|
777
818
|
}
|
|
778
819
|
|
|
779
820
|
function inferModelThinking<TApi extends Api>(model: ApiModel<TApi>): ThinkingConfig {
|
|
821
|
+
if (getMiniMaxThinkingMode(model) === "toggle") {
|
|
822
|
+
// The existing persisted effort value represents the single enabled state;
|
|
823
|
+
// UI consumers label it "on", and transports send no effort or budget.
|
|
824
|
+
return { mode: "effort", minLevel: Effort.High, maxLevel: Effort.High };
|
|
825
|
+
}
|
|
780
826
|
const parsedModel = parseKnownModel(model.id);
|
|
781
827
|
const efforts = inferSupportedEfforts(parsedModel, model);
|
|
782
828
|
const minLevel = efforts[0];
|
package/src/models.json
CHANGED
|
@@ -66611,6 +66611,31 @@
|
|
|
66611
66611
|
"minLevel": "minimal",
|
|
66612
66612
|
"maxLevel": "xhigh"
|
|
66613
66613
|
}
|
|
66614
|
+
},
|
|
66615
|
+
"union-alpha": {
|
|
66616
|
+
"id": "union-alpha",
|
|
66617
|
+
"name": "Union Alpha Free",
|
|
66618
|
+
"api": "anthropic-messages",
|
|
66619
|
+
"provider": "opencode-go",
|
|
66620
|
+
"baseUrl": "https://opencode.ai/zen/go",
|
|
66621
|
+
"reasoning": true,
|
|
66622
|
+
"input": [
|
|
66623
|
+
"text",
|
|
66624
|
+
"image"
|
|
66625
|
+
],
|
|
66626
|
+
"cost": {
|
|
66627
|
+
"input": 0,
|
|
66628
|
+
"output": 0,
|
|
66629
|
+
"cacheRead": 0,
|
|
66630
|
+
"cacheWrite": 0
|
|
66631
|
+
},
|
|
66632
|
+
"contextWindow": 262144,
|
|
66633
|
+
"maxTokens": 131072,
|
|
66634
|
+
"thinking": {
|
|
66635
|
+
"mode": "budget",
|
|
66636
|
+
"minLevel": "minimal",
|
|
66637
|
+
"maxLevel": "xhigh"
|
|
66638
|
+
}
|
|
66614
66639
|
}
|
|
66615
66640
|
},
|
|
66616
66641
|
"opencode-zen": {
|
|
@@ -68629,6 +68654,31 @@
|
|
|
68629
68654
|
},
|
|
68630
68655
|
"contextWindow": 131072,
|
|
68631
68656
|
"maxTokens": 131072
|
|
68657
|
+
},
|
|
68658
|
+
"union-alpha": {
|
|
68659
|
+
"id": "union-alpha",
|
|
68660
|
+
"name": "Union Alpha Free",
|
|
68661
|
+
"api": "anthropic-messages",
|
|
68662
|
+
"provider": "opencode-zen",
|
|
68663
|
+
"baseUrl": "https://opencode.ai/zen",
|
|
68664
|
+
"reasoning": true,
|
|
68665
|
+
"input": [
|
|
68666
|
+
"text",
|
|
68667
|
+
"image"
|
|
68668
|
+
],
|
|
68669
|
+
"cost": {
|
|
68670
|
+
"input": 0,
|
|
68671
|
+
"output": 0,
|
|
68672
|
+
"cacheRead": 0,
|
|
68673
|
+
"cacheWrite": 0
|
|
68674
|
+
},
|
|
68675
|
+
"contextWindow": 262144,
|
|
68676
|
+
"maxTokens": 131072,
|
|
68677
|
+
"thinking": {
|
|
68678
|
+
"mode": "budget",
|
|
68679
|
+
"minLevel": "minimal",
|
|
68680
|
+
"maxLevel": "xhigh"
|
|
68681
|
+
}
|
|
68632
68682
|
}
|
|
68633
68683
|
},
|
|
68634
68684
|
"opengateway": {
|
|
@@ -856,44 +856,41 @@ export interface OpenCodeModelManagerConfig {
|
|
|
856
856
|
}
|
|
857
857
|
|
|
858
858
|
function openCodeModelManagerOptions(
|
|
859
|
-
providerId: "opencode-go" | "opencode-zen"
|
|
859
|
+
providerId: "opencode-go" | "opencode-zen",
|
|
860
860
|
defaultBaseUrl: string,
|
|
861
861
|
config?: OpenCodeModelManagerConfig,
|
|
862
|
-
): ModelManagerOptions<
|
|
862
|
+
): ModelManagerOptions<Api> {
|
|
863
863
|
const apiKey = config?.apiKey;
|
|
864
864
|
const baseUrl = config?.baseUrl ?? defaultBaseUrl;
|
|
865
|
-
const references =
|
|
866
|
-
providerId === "opencode-go" ? createBundledReferenceMap<"openai-completions">(providerId) : undefined;
|
|
865
|
+
const references = createBundledReferenceMap<Api>(providerId);
|
|
867
866
|
return {
|
|
868
867
|
providerId,
|
|
869
868
|
...(apiKey && {
|
|
870
869
|
fetchDynamicModels: () =>
|
|
871
|
-
fetchOpenAICompatibleModels({
|
|
870
|
+
fetchOpenAICompatibleModels<Api>({
|
|
872
871
|
api: "openai-completions",
|
|
873
872
|
provider: providerId,
|
|
874
873
|
baseUrl,
|
|
875
874
|
apiKey,
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
875
|
+
mapModel: (entry, defaults) => {
|
|
876
|
+
const model = mapWithBundledReference(entry, defaults, references.get(defaults.id));
|
|
877
|
+
// Discovery uses /v1/models, but the Anthropic client appends
|
|
878
|
+
// /v1/messages itself. Preserve the configured origin, not /v1/v1.
|
|
879
|
+
if (model.api === "anthropic-messages") {
|
|
880
|
+
model.baseUrl = model.baseUrl.replace(/\/v1\/?$/u, "");
|
|
881
|
+
}
|
|
882
|
+
return providerId === "opencode-go" ? applyOpenCodeGoOfficialMetadata(model) : model;
|
|
883
|
+
},
|
|
883
884
|
}),
|
|
884
885
|
}),
|
|
885
886
|
};
|
|
886
887
|
}
|
|
887
888
|
|
|
888
|
-
export function opencodeZenModelManagerOptions(
|
|
889
|
-
config?: OpenCodeModelManagerConfig,
|
|
890
|
-
): ModelManagerOptions<"openai-completions"> {
|
|
889
|
+
export function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {
|
|
891
890
|
return openCodeModelManagerOptions("opencode-zen", "https://opencode.ai/zen/v1", config);
|
|
892
891
|
}
|
|
893
892
|
|
|
894
|
-
export function opencodeGoModelManagerOptions(
|
|
895
|
-
config?: OpenCodeModelManagerConfig,
|
|
896
|
-
): ModelManagerOptions<"openai-completions"> {
|
|
893
|
+
export function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {
|
|
897
894
|
return openCodeModelManagerOptions("opencode-go", "https://opencode.ai/zen/go/v1", config);
|
|
898
895
|
}
|
|
899
896
|
|
|
@@ -2378,7 +2375,9 @@ function createOpenCodeApiResolution(
|
|
|
2378
2375
|
}
|
|
2379
2376
|
|
|
2380
2377
|
const OPENCODE_GO_BASE_PATH = "https://opencode.ai/zen/go";
|
|
2381
|
-
const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen"
|
|
2378
|
+
const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen", {
|
|
2379
|
+
"union-alpha": "anthropic-messages",
|
|
2380
|
+
});
|
|
2382
2381
|
const OPENCODE_GO_CHAT_COMPLETIONS_MODEL_IDS = [
|
|
2383
2382
|
"deepseek-v4-flash",
|
|
2384
2383
|
"deepseek-v4-flash-vision-exp",
|
|
@@ -2401,6 +2400,7 @@ const OPENCODE_GO_MESSAGES_MODEL_IDS = [
|
|
|
2401
2400
|
"qwen3.7-max",
|
|
2402
2401
|
"qwen3.7-plus",
|
|
2403
2402
|
"qwen3.8-flash",
|
|
2403
|
+
"union-alpha",
|
|
2404
2404
|
] as const;
|
|
2405
2405
|
const OPENCODE_GO_API_OVERRIDES: Readonly<Record<string, Api>> = {
|
|
2406
2406
|
...Object.fromEntries(OPENCODE_GO_CHAT_COMPLETIONS_MODEL_IDS.map(id => [id, "openai-completions"])),
|
|
@@ -2658,6 +2658,14 @@ interface OpenCodeGoOfficialModelMetadata {
|
|
|
2658
2658
|
}
|
|
2659
2659
|
|
|
2660
2660
|
const OPENCODE_GO_OFFICIAL_MODELS: Readonly<Record<string, OpenCodeGoOfficialModelMetadata>> = {
|
|
2661
|
+
"union-alpha": {
|
|
2662
|
+
name: "Union Alpha Free",
|
|
2663
|
+
contextWindow: 262_144,
|
|
2664
|
+
maxTokens: 131_072,
|
|
2665
|
+
input: ["text", "image"],
|
|
2666
|
+
reasoning: true,
|
|
2667
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2668
|
+
},
|
|
2661
2669
|
"deepseek-v4-flash": {
|
|
2662
2670
|
name: "DeepSeek V4 Flash",
|
|
2663
2671
|
contextWindow: 1_000_000,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
PROVIDER_SAFETY_STOP_ADAPTER_CAPABILITY,
|
|
27
27
|
} from "../adapter-internals/provider-safety-stop";
|
|
28
28
|
import {
|
|
29
|
+
getMiniMaxThinkingMode,
|
|
29
30
|
hasOpus47ApiRestrictions,
|
|
30
31
|
mapEffortToAnthropicAdaptiveEffort,
|
|
31
32
|
supportsAnthropicAdaptiveThinkingDisplay as supportsAdaptiveThinkingDisplay,
|
|
@@ -3610,7 +3611,14 @@ function buildParams(
|
|
|
3610
3611
|
);
|
|
3611
3612
|
}
|
|
3612
3613
|
|
|
3613
|
-
|
|
3614
|
+
const miniMaxMode = getMiniMaxThinkingMode(model, baseUrl);
|
|
3615
|
+
if (model.reasoning && miniMaxMode === "toggle") {
|
|
3616
|
+
// MiniMax's Messages endpoint defaults to off and only supports a
|
|
3617
|
+
// switch. Claude budget_tokens/output_config.effort do not apply.
|
|
3618
|
+
if (options?.thinkingEnabled !== undefined) {
|
|
3619
|
+
params.thinking = { type: options.thinkingEnabled ? "adaptive" : "disabled" };
|
|
3620
|
+
}
|
|
3621
|
+
} else if (model.reasoning && miniMaxMode !== "always-on") {
|
|
3614
3622
|
if (options?.thinkingEnabled) {
|
|
3615
3623
|
const mode = model.thinking?.mode;
|
|
3616
3624
|
const requestedEffort = options.reasoning;
|
package/src/providers/cursor.ts
CHANGED
|
@@ -676,6 +676,12 @@ const CURSOR_WRITE_DRAIN_TIMEOUT_MS = 5_000;
|
|
|
676
676
|
const CURSOR_MAX_PENDING_SHELL_WRITE_BYTES = 1024 * 1024;
|
|
677
677
|
const pendingCursorWrites = new WeakMap<object, Set<Promise<void>>>();
|
|
678
678
|
const cursorWriteErrors = new WeakMap<object, unknown>();
|
|
679
|
+
interface CursorWriteListeners {
|
|
680
|
+
finishes: Set<(error?: unknown) => void>;
|
|
681
|
+
onError: (error: unknown) => void;
|
|
682
|
+
onClose: () => void;
|
|
683
|
+
}
|
|
684
|
+
const cursorWriteListeners = new WeakMap<object, CursorWriteListeners>();
|
|
679
685
|
|
|
680
686
|
function closeStalledCursorRequest(request: http2.ClientHttp2Stream): void {
|
|
681
687
|
// A request whose peer stopped reading may never invoke a write callback. Close
|
|
@@ -833,25 +839,44 @@ function writeCursorFrame(request: http2.ClientHttp2Stream, frame: Uint8Array):
|
|
|
833
839
|
// late transport error cannot surface as an unhandled rejection in the gap.
|
|
834
840
|
completion.promise.catch(() => {});
|
|
835
841
|
pending.add(completion.promise);
|
|
842
|
+
let listeners = cursorWriteListeners.get(request);
|
|
843
|
+
if (!listeners) {
|
|
844
|
+
const finishes = new Set<(error?: unknown) => void>();
|
|
845
|
+
const onError = (error: unknown) => {
|
|
846
|
+
for (const finish of [...finishes]) finish(error);
|
|
847
|
+
};
|
|
848
|
+
listeners = {
|
|
849
|
+
finishes,
|
|
850
|
+
onError,
|
|
851
|
+
onClose: () => onError(new Error("Cursor request closed before write completed")),
|
|
852
|
+
};
|
|
853
|
+
cursorWriteListeners.set(request, listeners);
|
|
854
|
+
}
|
|
855
|
+
const shared = listeners;
|
|
836
856
|
const finish = (error?: unknown) => {
|
|
837
857
|
if (completed) return;
|
|
838
858
|
completed = true;
|
|
839
859
|
pending.delete(completion.promise);
|
|
840
860
|
if (error != null && !cursorWriteErrors.has(request)) cursorWriteErrors.set(request, error);
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
request.removeListener
|
|
861
|
+
shared.finishes.delete(finish);
|
|
862
|
+
if (shared.finishes.size === 0) {
|
|
863
|
+
if (typeof request.removeListener === "function") {
|
|
864
|
+
request.removeListener("close", shared.onClose);
|
|
865
|
+
request.removeListener("error", shared.onError);
|
|
866
|
+
}
|
|
867
|
+
cursorWriteListeners.delete(request);
|
|
868
|
+
// Keep the pending set and first error until the final drain observes them.
|
|
844
869
|
}
|
|
845
870
|
if (error == null) completion.resolve();
|
|
846
871
|
else completion.reject(error);
|
|
847
872
|
};
|
|
848
|
-
|
|
873
|
+
shared.finishes.add(finish);
|
|
849
874
|
try {
|
|
850
875
|
// The real HTTP/2 stream always exposes EventEmitter methods. Keep the
|
|
851
876
|
// test seam tolerant of a minimal writer stub as well.
|
|
852
|
-
if (typeof request.once === "function") {
|
|
853
|
-
request.once("close", onClose);
|
|
854
|
-
request.once("error",
|
|
877
|
+
if (shared.finishes.size === 1 && typeof request.once === "function") {
|
|
878
|
+
request.once("close", shared.onClose);
|
|
879
|
+
request.once("error", shared.onError);
|
|
855
880
|
}
|
|
856
881
|
return request.write(frame, finish) !== false;
|
|
857
882
|
} catch (error) {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "../adapter-internals/provider-safety-stop";
|
|
19
19
|
import {
|
|
20
20
|
type Effort,
|
|
21
|
+
getMiniMaxThinkingMode,
|
|
21
22
|
getSupportedEfforts,
|
|
22
23
|
isGroqCompoundReasoningUnsupported,
|
|
23
24
|
modelSupportsReasoningControl,
|
|
@@ -364,7 +365,7 @@ type OpenAICompletionsParams = Omit<OpenAI.Chat.Completions.ChatCompletionCreate
|
|
|
364
365
|
top_k?: number;
|
|
365
366
|
min_p?: number;
|
|
366
367
|
repetition_penalty?: number;
|
|
367
|
-
thinking?: { type: "enabled" | "disabled" };
|
|
368
|
+
thinking?: { type: "enabled" | "disabled" | "adaptive" };
|
|
368
369
|
enable_thinking?: boolean;
|
|
369
370
|
chat_template_kwargs?: { enable_thinking: boolean };
|
|
370
371
|
reasoning?: { effort?: string } | { enabled: false };
|
|
@@ -1576,7 +1577,12 @@ function buildParams(
|
|
|
1576
1577
|
delete params.tool_choice;
|
|
1577
1578
|
}
|
|
1578
1579
|
|
|
1579
|
-
if (supportsReasoningParams &&
|
|
1580
|
+
if (supportsReasoningParams && getMiniMaxThinkingMode(model, resolvedBaseUrl) === "toggle") {
|
|
1581
|
+
// MiniMax-M3 accepts an on/off switch, not reasoning_effort. Omitting
|
|
1582
|
+
// the switch preserves the OpenAI-compatible endpoint's default (on).
|
|
1583
|
+
if (options?.disableReasoning) params.thinking = { type: "disabled" };
|
|
1584
|
+
else if (options?.reasoning) params.thinking = { type: "adaptive" };
|
|
1585
|
+
} else if (supportsReasoningParams && compat.thinkingFormat === "zai" && model.reasoning) {
|
|
1580
1586
|
// Z.ai uses binary thinking: { type: "enabled" | "disabled" }
|
|
1581
1587
|
// Must explicitly disable since z.ai defaults to thinking enabled.
|
|
1582
1588
|
const enabled = options?.reasoning && !options?.disableReasoning;
|
|
@@ -31,6 +31,11 @@ interface CatalogRow {
|
|
|
31
31
|
maxTokens?: unknown;
|
|
32
32
|
reasoning?: unknown;
|
|
33
33
|
input?: unknown;
|
|
34
|
+
capabilities?: {
|
|
35
|
+
context_length?: unknown;
|
|
36
|
+
input_modalities?: unknown;
|
|
37
|
+
supports_reasoning?: unknown;
|
|
38
|
+
};
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
export interface OpenCodexEndpoint {
|
|
@@ -120,6 +125,9 @@ function asPositiveNumber(value: unknown, fallback: number): number {
|
|
|
120
125
|
|
|
121
126
|
function normalizeCatalogPayload(payload: unknown): CatalogRow[] {
|
|
122
127
|
if (Array.isArray(payload)) return payload as CatalogRow[];
|
|
128
|
+
if (payload && typeof payload === "object" && Array.isArray((payload as { data?: unknown }).data)) {
|
|
129
|
+
return (payload as { data: CatalogRow[] }).data;
|
|
130
|
+
}
|
|
123
131
|
if (payload && typeof payload === "object" && Array.isArray((payload as { models?: unknown }).models)) {
|
|
124
132
|
return (payload as { models: CatalogRow[] }).models;
|
|
125
133
|
}
|
|
@@ -127,12 +135,14 @@ function normalizeCatalogPayload(payload: unknown): CatalogRow[] {
|
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
function normalizeModel(row: CatalogRow, endpoint: OpenCodexEndpoint): Model<"openai-responses"> | undefined {
|
|
138
|
+
if (!row || typeof row !== "object") return undefined;
|
|
130
139
|
const rawId = typeof row.id === "string" ? row.id.trim() : typeof row.model === "string" ? row.model.trim() : "";
|
|
131
140
|
if (!rawId || rawId.includes("\n")) return undefined;
|
|
132
141
|
const publicId = `opencodex/${rawId}`;
|
|
142
|
+
const modalities = row.input ?? row.capabilities?.input_modalities;
|
|
133
143
|
const input =
|
|
134
|
-
Array.isArray(
|
|
135
|
-
?
|
|
144
|
+
Array.isArray(modalities) && modalities.every(value => value === "text" || value === "image")
|
|
145
|
+
? modalities
|
|
136
146
|
: ["text"];
|
|
137
147
|
return {
|
|
138
148
|
id: publicId,
|
|
@@ -142,10 +152,10 @@ function normalizeModel(row: CatalogRow, endpoint: OpenCodexEndpoint): Model<"op
|
|
|
142
152
|
provider: "opencodex",
|
|
143
153
|
baseUrl: `${endpoint.baseUrl}/v1`,
|
|
144
154
|
compat: { supportsServiceTier: true },
|
|
145
|
-
reasoning: row.reasoning !== false,
|
|
155
|
+
reasoning: (row.reasoning ?? row.capabilities?.supports_reasoning) !== false,
|
|
146
156
|
input,
|
|
147
157
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
148
|
-
contextWindow: asPositiveNumber(row.contextWindow, 128_000),
|
|
158
|
+
contextWindow: asPositiveNumber(row.contextWindow ?? row.capabilities?.context_length, 128_000),
|
|
149
159
|
maxTokens: asPositiveNumber(row.maxTokens, 16_384),
|
|
150
160
|
};
|
|
151
161
|
}
|
|
@@ -154,7 +164,7 @@ export async function fetchOpenCodexModels(): Promise<readonly Model<"openai-res
|
|
|
154
164
|
const endpoint = await resolveOpenCodexEndpoint();
|
|
155
165
|
if (!endpoint) return null;
|
|
156
166
|
try {
|
|
157
|
-
const rows = normalizeCatalogPayload(await fetchJson(`${endpoint.baseUrl}/
|
|
167
|
+
const rows = normalizeCatalogPayload(await fetchJson(`${endpoint.baseUrl}/v1/models`));
|
|
158
168
|
const models = rows
|
|
159
169
|
.map(row => normalizeModel(row, endpoint))
|
|
160
170
|
.filter((model): model is Model<"openai-responses"> => model !== undefined);
|
package/src/stream.ts
CHANGED
|
@@ -27,6 +27,7 @@ function markManagedAttemptValidated<T extends object>(options: T): T {
|
|
|
27
27
|
import { getCustomApi } from "./api-registry";
|
|
28
28
|
import type { Effort } from "./model-thinking";
|
|
29
29
|
import {
|
|
30
|
+
getMiniMaxThinkingMode,
|
|
30
31
|
mapEffortToAnthropicAdaptiveEffort,
|
|
31
32
|
mapEffortToGoogleThinkingLevel,
|
|
32
33
|
requireSupportedEffort,
|
|
@@ -978,6 +979,16 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
978
979
|
|
|
979
980
|
switch (model.api) {
|
|
980
981
|
case "anthropic-messages": {
|
|
982
|
+
const miniMaxMode = getMiniMaxThinkingMode(model);
|
|
983
|
+
if (miniMaxMode) {
|
|
984
|
+
return castApi<"anthropic-messages">({
|
|
985
|
+
...base,
|
|
986
|
+
thinkingEnabled:
|
|
987
|
+
miniMaxMode === "toggle" ? !!options?.reasoning && !options?.disableReasoning : undefined,
|
|
988
|
+
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
989
|
+
serviceTier: options?.serviceTier,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
981
992
|
// Explicitly disable thinking when reasoning is not specified or model doesn't support it
|
|
982
993
|
const reasoning = options?.reasoning;
|
|
983
994
|
if (!reasoning || !model.reasoning) {
|
|
@@ -1094,7 +1105,12 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1094
1105
|
return castApi<"openai-completions">({
|
|
1095
1106
|
...base,
|
|
1096
1107
|
reasoning: resolveOpenAiReasoningEffort(model, options),
|
|
1097
|
-
|
|
1108
|
+
// Agent-level off is represented by an absent effort. MiniMax's
|
|
1109
|
+
// native OpenAI endpoint defaults to on, so send an explicit switch.
|
|
1110
|
+
disableReasoning:
|
|
1111
|
+
getMiniMaxThinkingMode(model) === "toggle"
|
|
1112
|
+
? !options?.reasoning || options?.disableReasoning
|
|
1113
|
+
: options?.disableReasoning,
|
|
1098
1114
|
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
|
|
1099
1115
|
serviceTier: options?.serviceTier,
|
|
1100
1116
|
});
|
package/src/utils/h2-fetch.ts
CHANGED
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
* advertise h2 via ALPN but then refuse or reset the connection at the HTTP/2
|
|
13
13
|
* framing layer. Bun surfaces these as `ConnectionRefused`, `ConnectionReset`,
|
|
14
14
|
* or `ConnectionClosed` rather than `HTTP2Unsupported`, so we treat those
|
|
15
|
-
* codes as h2-fallback triggers as well.
|
|
15
|
+
* codes as h2-fallback triggers as well. `ConnectionRefused` is raised before
|
|
16
|
+
* the request is written, but a reset or a close does not prove the peer never
|
|
17
|
+
* consumed the body — it may have processed the request and died before
|
|
18
|
+
* answering. Replaying those two on h1 would duplicate the side effect, so
|
|
19
|
+
* `ConnectionReset` and `ConnectionClosed` fall back only for replay-safe
|
|
20
|
+
* methods; anything else rethrows the original error.
|
|
16
21
|
*
|
|
17
22
|
* ALPN-refusing hosts (notably zcode.z.ai, the GLM ZCode OAuth broker) abort
|
|
18
23
|
* the TLS handshake entirely when the client offers ALPN h2. Bun reports that
|
|
@@ -47,12 +52,16 @@ export function installH2Fetch(): void {
|
|
|
47
52
|
// code; the h1 fallback below re-verifies the certificate itself.
|
|
48
53
|
"UNKNOWN_CERTIFICATE_VERIFICATION_ERROR",
|
|
49
54
|
]);
|
|
55
|
+
/** Fallback codes that may fire *after* the peer consumed the body — replay only when safe. */
|
|
56
|
+
const replayGatedCodes: ReadonlySet<string> = new Set(["ConnectionReset", "ConnectionClosed"]);
|
|
50
57
|
const wrapper = async function h2fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
|
|
51
58
|
if (!isHttps(input)) return original(input, init);
|
|
52
59
|
try {
|
|
53
60
|
return await original(input, { ...init, protocol: "http2" });
|
|
54
61
|
} catch (err) {
|
|
55
|
-
|
|
62
|
+
const code = (err as { code?: string }).code ?? "";
|
|
63
|
+
if (!h2FallbackCodes.has(code)) throw err;
|
|
64
|
+
if (replayGatedCodes.has(code) && !isReplaySafeRequest(input, init)) throw err;
|
|
56
65
|
return original(input, init);
|
|
57
66
|
}
|
|
58
67
|
} as typeof fetch & PatchedFetch;
|
|
@@ -63,6 +72,21 @@ export function installH2Fetch(): void {
|
|
|
63
72
|
globalThis.fetch = wrapper;
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
/** Methods a transport-layer retry cannot turn into a second side effect. */
|
|
76
|
+
const replaySafeMethods: ReadonlySet<string> = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether replaying this request on a fresh connection is side-effect free.
|
|
80
|
+
*
|
|
81
|
+
* PR #5614 introduces an identically-named helper with the same semantics for
|
|
82
|
+
* `HTTP2StreamReset`; whichever of the two lands second should collapse into
|
|
83
|
+
* this one rather than leaving the repo with two devices doing the same job.
|
|
84
|
+
*/
|
|
85
|
+
function isReplaySafeRequest(input: string | URL | Request, init?: RequestInit): boolean {
|
|
86
|
+
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
87
|
+
return replaySafeMethods.has(method.toUpperCase());
|
|
88
|
+
}
|
|
89
|
+
|
|
66
90
|
function isHttps(input: string | URL | Request): boolean {
|
|
67
91
|
if (typeof input === "string") return input.startsWith("https:");
|
|
68
92
|
if (input instanceof URL) return input.protocol === "https:";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { APP_NAME, extractHttpStatusFromError,
|
|
3
|
+
import { APP_NAME, extractHttpStatusFromError, getEffectiveLogsDir } from "@gajae-code/utils";
|
|
4
4
|
import { isCopilotTransientModelError } from "./retry.js";
|
|
5
5
|
import { formatErrorMessageWithRetryAfter } from "./retry-after.js";
|
|
6
6
|
|
|
@@ -109,7 +109,7 @@ const MAX_RETAINED_DUMPS = 50;
|
|
|
109
109
|
|
|
110
110
|
/** Directory holding the retained HTTP 400 dumps. */
|
|
111
111
|
export function httpRequestDumpDir(): string {
|
|
112
|
-
return path.join(
|
|
112
|
+
return path.join(getEffectiveLogsDir(), "http-400-requests");
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|