@oh-my-pi/pi-ai 17.0.2 → 17.0.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 +11 -0
- package/dist/types/providers/cursor.d.ts +14 -0
- package/dist/types/providers/kimi.d.ts +3 -3
- package/dist/types/providers/openai-anthropic-shim.d.ts +3 -1
- package/dist/types/providers/openai-shared.d.ts +2 -0
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/event-stream.d.ts +2 -0
- package/package.json +4 -4
- package/src/providers/__tests__/kimi-code-thinking.test.ts +119 -2
- package/src/providers/cursor.ts +31 -2
- package/src/providers/kimi.ts +5 -4
- package/src/providers/openai-anthropic-shim.ts +7 -1
- package/src/providers/openai-shared.ts +26 -4
- package/src/stream.ts +9 -4
- package/src/types.ts +1 -1
- package/src/usage/claude.ts +10 -24
- package/src/utils/event-stream.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.0.3] - 2026-07-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Replaced the opaque `h2 is not supported` failure on the Cursor run transport with an actionable error naming the ALPN-stripping proxy as the cause and pointing at the `providers.cursor.baseUrl` HTTP/2 bridge workaround. The run RPC is HTTP/2-only, so behind a TLS-intercepting proxy that strips ALPN (e.g. Zscaler) bun cannot negotiate `h2` and the completion cannot proceed ([#5828](https://github.com/can1357/oh-my-pi/issues/5828)).
|
|
10
|
+
- Restored the `createAssistantMessageEventStream()` root export used by legacy provider extensions ([#5879](https://github.com/can1357/oh-my-pi/issues/5879)).
|
|
11
|
+
- Fixed parallel Responses tool-result images interleaving synthetic user messages before all pending outputs, preventing strict OpenRouter/Moonshot backends from rejecting follow-up requests. ([#5850](https://github.com/can1357/oh-my-pi/issues/5850))
|
|
12
|
+
- Fixed Kimi Code K3 requests to send native named efforts (`low`, `high`, `max`) and use adaptive effort rather than generic token budgets on explicit Anthropic transport overrides ([#5893](https://github.com/can1357/oh-my-pi/issues/5893)).
|
|
13
|
+
- Automatically invalidate and rotate OAuth credentials when an "invalidated oauth token" error occurs
|
|
14
|
+
- Fixed Anthropic usage reports treating the organization response header as the account identity, which caused the 5h/7d status-line segment to disappear for OAuth credentials without stored organization metadata. ([#5698](https://github.com/can1357/oh-my-pi/issues/5698))
|
|
15
|
+
|
|
5
16
|
## [17.0.2] - 2026-07-17
|
|
6
17
|
|
|
7
18
|
### Fixed
|
|
@@ -13,6 +13,20 @@ export interface CursorOptions extends StreamOptions {
|
|
|
13
13
|
execHandlers?: CursorExecHandlers;
|
|
14
14
|
onToolResult?: CursorToolResultHandler;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Maps an opaque HTTP/2 negotiation failure into an actionable error.
|
|
18
|
+
*
|
|
19
|
+
* bun only opens an HTTP/2 session when TLS-ALPN negotiates `h2`. Behind a
|
|
20
|
+
* TLS-intercepting proxy that strips ALPN (e.g. Zscaler), the handshake yields
|
|
21
|
+
* no `h2` protocol and bun throws `ERR_HTTP2_ERROR: h2 is not supported`. The
|
|
22
|
+
* Cursor run RPC is HTTP/2-only (the ALB rejects HTTP/1.1 with 464), so there
|
|
23
|
+
* is no h1 fallback the way model discovery has one — the run simply cannot
|
|
24
|
+
* proceed. Replace the opaque message with one that names the cause and points
|
|
25
|
+
* at the `providers.cursor.baseUrl` workaround.
|
|
26
|
+
*
|
|
27
|
+
* Non-ALPN errors pass through untouched.
|
|
28
|
+
*/
|
|
29
|
+
export declare function mapH2TransportError(error: unknown, baseUrl: string): unknown;
|
|
16
30
|
export declare const streamCursor: StreamFunction<"cursor-agent">;
|
|
17
31
|
export type ToolCallState = ToolCall & {
|
|
18
32
|
[kStreamingBlockIndex]: number;
|
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
* - OpenAI: https://api.kimi.com/coding/v1/chat/completions
|
|
6
6
|
* - Anthropic: https://api.kimi.com/coding/v1/messages
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Each discovered model selects its server-declared protocol; legacy models
|
|
9
|
+
* without protocol metadata retain the Anthropic-compatible default.
|
|
10
10
|
*/
|
|
11
11
|
import type { Api, Context, Model } from "../types.js";
|
|
12
12
|
import type { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
13
13
|
import { type OpenAIAnthropicApiFormat, type OpenAIAnthropicShimOptions } from "./openai-anthropic-shim.js";
|
|
14
14
|
export type KimiApiFormat = OpenAIAnthropicApiFormat;
|
|
15
15
|
export interface KimiOptions extends OpenAIAnthropicShimOptions {
|
|
16
|
-
/** API format
|
|
16
|
+
/** Explicit API format override. Defaults to the model's discovered protocol. */
|
|
17
17
|
format?: KimiApiFormat;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* format, optional extra headers); the streaming/forwarding plumbing lives
|
|
8
8
|
* here once.
|
|
9
9
|
*/
|
|
10
|
-
import type { Context, Model, SimpleStreamOptions } from "../types.js";
|
|
10
|
+
import type { Context, Model, SimpleStreamOptions, ThinkingControlMode } from "../types.js";
|
|
11
11
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
12
12
|
export type OpenAIAnthropicApiFormat = "openai" | "anthropic";
|
|
13
13
|
export interface OpenAIAnthropicShimOptions extends SimpleStreamOptions {
|
|
@@ -21,6 +21,8 @@ export interface OpenAIAnthropicShimConfig {
|
|
|
21
21
|
openaiBaseUrl?: string;
|
|
22
22
|
/** Default API format when caller does not specify one. */
|
|
23
23
|
defaultFormat: OpenAIAnthropicApiFormat;
|
|
24
|
+
/** Thinking transport used when this provider's Anthropic endpoint differs from generic budget semantics. */
|
|
25
|
+
anthropicThinkingMode?: ThinkingControlMode;
|
|
24
26
|
/** Provider-specific headers (e.g. auth/session) merged ahead of user-supplied headers. */
|
|
25
27
|
extraHeaders?: () => Record<string, string>;
|
|
26
28
|
}
|
|
@@ -200,6 +200,7 @@ export type OpenAICompletionsParams = Omit<ChatCompletionCreateParamsStreaming,
|
|
|
200
200
|
repetition_penalty?: number;
|
|
201
201
|
thinking?: {
|
|
202
202
|
type: "enabled" | "disabled";
|
|
203
|
+
effort?: string;
|
|
203
204
|
keep?: "all";
|
|
204
205
|
};
|
|
205
206
|
enable_thinking?: boolean;
|
|
@@ -382,6 +383,7 @@ export interface BuildResponsesInputOptions<TApi extends Api> {
|
|
|
382
383
|
}
|
|
383
384
|
export declare function buildResponsesInput<TApi extends Api>(options: BuildResponsesInputOptions<TApi>): ResponseInput;
|
|
384
385
|
export declare function convertResponsesAssistantMessage<TApi extends Api>(assistantMsg: AssistantMessage, model: Model<TApi>, msgIndex: number, knownCallIds: Set<string>, includeThinkingSignatures?: boolean, customCallIds?: Set<string>, preserveMessageIds?: boolean, supportsCustomToolCalls?: boolean, customToolWireNameMap?: ReadonlyMap<string, string>): ResponseInput;
|
|
386
|
+
/** Appends one tool result while keeping consecutive outputs ahead of its synthetic image messages. */
|
|
385
387
|
export declare function appendResponsesToolResultMessages<TApi extends Api>(messages: ResponseInput, toolResult: ToolResultMessage, model: Model<TApi>, strictResponsesPairing: boolean, supportsImageDetailOriginal: boolean, knownCallIds: ReadonlySet<string>, customCallIds?: ReadonlySet<string>, supportsCustomToolCalls?: boolean): void;
|
|
386
388
|
/**
|
|
387
389
|
* Per-block accumulation helpers shared by the two Responses decode loops —
|
package/dist/types/types.d.ts
CHANGED
|
@@ -376,7 +376,7 @@ export interface SimpleStreamOptions extends Omit<StreamOptions, "apiKey"> {
|
|
|
376
376
|
toolChoice?: ToolChoice;
|
|
377
377
|
/** OpenAI service tier for processing priority/cost control. Ignored by non-OpenAI providers. */
|
|
378
378
|
serviceTier?: ServiceTier;
|
|
379
|
-
/** API format
|
|
379
|
+
/** Explicit Kimi Code API format override; omitted uses live per-model protocol metadata. */
|
|
380
380
|
kimiApiFormat?: "openai" | "anthropic";
|
|
381
381
|
/** API format for Synthetic provider: "openai" or "anthropic" (default: "openai") */
|
|
382
382
|
syntheticApiFormat?: "openai" | "anthropic";
|
|
@@ -35,3 +35,5 @@ export declare class AssistantMessageEventStream extends EventStream<AssistantMe
|
|
|
35
35
|
push(event: AssistantMessageEvent): void;
|
|
36
36
|
end(result?: AssistantMessage): void;
|
|
37
37
|
}
|
|
38
|
+
/** Create an assistant-message event stream for legacy extension providers. */
|
|
39
|
+
export declare function createAssistantMessageEventStream(): AssistantMessageEventStream;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "17.0.
|
|
4
|
+
"version": "17.0.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",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.1",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
42
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
43
|
-
"@oh-my-pi/pi-wire": "17.0.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "17.0.3",
|
|
42
|
+
"@oh-my-pi/pi-utils": "17.0.3",
|
|
43
|
+
"@oh-my-pi/pi-wire": "17.0.3",
|
|
44
44
|
"arktype": "2.2.3",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
2
2
|
import { getBundledModel } from "@oh-my-pi/pi-catalog";
|
|
3
|
+
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
|
4
|
+
import { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
5
|
+
import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
|
|
3
6
|
import * as kimiOauth from "../../registry/oauth/kimi";
|
|
4
|
-
import
|
|
7
|
+
import { streamSimple } from "../../stream";
|
|
8
|
+
import type { Context, Model } from "../../types";
|
|
5
9
|
import type { MessageCreateParamsStreaming } from "../anthropic-wire";
|
|
6
|
-
import { streamKimi } from "../kimi";
|
|
10
|
+
import { type KimiApiFormat, streamKimi } from "../kimi";
|
|
7
11
|
import { streamOpenAIAnthropicShim } from "../openai-anthropic-shim";
|
|
8
12
|
import {
|
|
9
13
|
applyChatCompletionsCompatPolicy,
|
|
@@ -38,10 +42,123 @@ const TITLE_CONTEXT: Context = {
|
|
|
38
42
|
],
|
|
39
43
|
};
|
|
40
44
|
|
|
45
|
+
const K3_MODEL = buildModel({
|
|
46
|
+
id: "k3",
|
|
47
|
+
name: "K3",
|
|
48
|
+
api: "openai-completions",
|
|
49
|
+
provider: "kimi-code",
|
|
50
|
+
baseUrl: "https://api.kimi.com/coding/v1",
|
|
51
|
+
reasoning: true,
|
|
52
|
+
input: ["text"],
|
|
53
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
54
|
+
contextWindow: 1_048_576,
|
|
55
|
+
maxTokens: 32_000,
|
|
56
|
+
thinking: {
|
|
57
|
+
mode: "effort",
|
|
58
|
+
efforts: [Effort.Low, Effort.High, Effort.Max],
|
|
59
|
+
defaultLevel: Effort.Max,
|
|
60
|
+
requiresEffort: true,
|
|
61
|
+
},
|
|
62
|
+
compat: {
|
|
63
|
+
thinkingFormat: "kimi",
|
|
64
|
+
kimiApiFormat: "openai",
|
|
65
|
+
reasoningContentField: "reasoning_content",
|
|
66
|
+
supportsDeveloperRole: false,
|
|
67
|
+
},
|
|
68
|
+
} satisfies ModelSpec<"openai-completions">);
|
|
69
|
+
|
|
70
|
+
async function captureKimiPayload(
|
|
71
|
+
model: Model<"openai-completions">,
|
|
72
|
+
reasoning: Effort,
|
|
73
|
+
format?: KimiApiFormat,
|
|
74
|
+
): Promise<unknown> {
|
|
75
|
+
let payload: unknown;
|
|
76
|
+
const stream = streamKimi(
|
|
77
|
+
model,
|
|
78
|
+
{
|
|
79
|
+
systemPrompt: [],
|
|
80
|
+
messages: [{ role: "user", content: "Reply OK", timestamp: 0 }],
|
|
81
|
+
tools: [],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
...(format ? { format } : {}),
|
|
85
|
+
apiKey: "test-key",
|
|
86
|
+
reasoning,
|
|
87
|
+
onPayload: body => {
|
|
88
|
+
payload = body;
|
|
89
|
+
throw new Error("stop after payload capture");
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
await stream.result();
|
|
94
|
+
if (payload === undefined) throw new Error("Kimi request payload was not captured");
|
|
95
|
+
return payload;
|
|
96
|
+
}
|
|
97
|
+
|
|
41
98
|
afterEach(() => {
|
|
42
99
|
vi.restoreAllMocks();
|
|
43
100
|
});
|
|
44
101
|
|
|
102
|
+
describe("Kimi K3 thinking transport", () => {
|
|
103
|
+
it("sends every live named effort through Kimi's native thinking object by default", async () => {
|
|
104
|
+
vi.spyOn(kimiOauth, "getKimiCommonHeaders").mockReturnValue(KIMI_HEADERS);
|
|
105
|
+
|
|
106
|
+
for (const effort of [Effort.Low, Effort.High, Effort.Max]) {
|
|
107
|
+
const payload = await captureKimiPayload(K3_MODEL, effort);
|
|
108
|
+
expect(payload).toMatchObject({ thinking: { type: "enabled", effort } });
|
|
109
|
+
expect(payload).not.toHaveProperty("reasoning_effort");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("uses adaptive named effort rather than a token budget for an explicit Anthropic override", async () => {
|
|
114
|
+
vi.spyOn(kimiOauth, "getKimiCommonHeaders").mockReturnValue(KIMI_HEADERS);
|
|
115
|
+
|
|
116
|
+
const payload = await captureKimiPayload(K3_MODEL, Effort.Max, "anthropic");
|
|
117
|
+
|
|
118
|
+
expect(payload).toMatchObject({
|
|
119
|
+
thinking: { type: "adaptive" },
|
|
120
|
+
output_config: { effort: Effort.Max },
|
|
121
|
+
});
|
|
122
|
+
expect(payload).not.toHaveProperty("thinking.budget_tokens");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("keeps the legacy K2 default on the Anthropic transport", async () => {
|
|
126
|
+
vi.spyOn(kimiOauth, "getKimiCommonHeaders").mockReturnValue(KIMI_HEADERS);
|
|
127
|
+
const model = getBundledModel<"openai-completions">("kimi-code", "kimi-for-coding");
|
|
128
|
+
|
|
129
|
+
const payload = await captureKimiPayload(model, Effort.High);
|
|
130
|
+
|
|
131
|
+
expect(payload).toMatchObject({ thinking: { type: "enabled" } });
|
|
132
|
+
expect(payload).toHaveProperty("thinking.budget_tokens");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("clamps disabled thinking to the lowest effort for a mandatory-thinking K3", async () => {
|
|
136
|
+
vi.spyOn(kimiOauth, "getKimiCommonHeaders").mockReturnValue(KIMI_HEADERS);
|
|
137
|
+
|
|
138
|
+
let payload: unknown;
|
|
139
|
+
const stream = streamSimple(
|
|
140
|
+
K3_MODEL,
|
|
141
|
+
{
|
|
142
|
+
systemPrompt: [],
|
|
143
|
+
messages: [{ role: "user", content: "Reply OK", timestamp: 0 }],
|
|
144
|
+
tools: [],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
apiKey: "test-key",
|
|
148
|
+
disableReasoning: true,
|
|
149
|
+
onPayload: body => {
|
|
150
|
+
payload = body;
|
|
151
|
+
throw new Error("stop after payload capture");
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
await stream.result();
|
|
156
|
+
|
|
157
|
+
expect(payload).toMatchObject({ thinking: { type: "enabled", effort: Effort.Low } });
|
|
158
|
+
expect(payload).not.toMatchObject({ thinking: { type: "disabled" } });
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
45
162
|
describe("Kimi K2.7 Code thinking policy", () => {
|
|
46
163
|
it("expresses disabled thinking explicitly for title-generator-style Kimi Code requests", () => {
|
|
47
164
|
const model = getBundledModel<"openai-completions">("kimi-code", "kimi-for-coding");
|
package/src/providers/cursor.ts
CHANGED
|
@@ -213,6 +213,34 @@ function parseConnectEndStream(data: Uint8Array): Error | null {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Maps an opaque HTTP/2 negotiation failure into an actionable error.
|
|
218
|
+
*
|
|
219
|
+
* bun only opens an HTTP/2 session when TLS-ALPN negotiates `h2`. Behind a
|
|
220
|
+
* TLS-intercepting proxy that strips ALPN (e.g. Zscaler), the handshake yields
|
|
221
|
+
* no `h2` protocol and bun throws `ERR_HTTP2_ERROR: h2 is not supported`. The
|
|
222
|
+
* Cursor run RPC is HTTP/2-only (the ALB rejects HTTP/1.1 with 464), so there
|
|
223
|
+
* is no h1 fallback the way model discovery has one — the run simply cannot
|
|
224
|
+
* proceed. Replace the opaque message with one that names the cause and points
|
|
225
|
+
* at the `providers.cursor.baseUrl` workaround.
|
|
226
|
+
*
|
|
227
|
+
* Non-ALPN errors pass through untouched.
|
|
228
|
+
*/
|
|
229
|
+
export function mapH2TransportError(error: unknown, baseUrl: string): unknown {
|
|
230
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
231
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
232
|
+
if (code === "ERR_HTTP2_ERROR" && /h2 is not supported/i.test(message)) {
|
|
233
|
+
return new AIError.ProviderResponseError(
|
|
234
|
+
`Cursor run transport could not negotiate HTTP/2 with ${baseUrl}: "h2 is not supported". ` +
|
|
235
|
+
"This host serves the run RPC over HTTP/2 only, and the TLS handshake did not negotiate " +
|
|
236
|
+
"h2 via ALPN — typically an ALPN-stripping TLS-intercepting proxy (e.g. Zscaler). " +
|
|
237
|
+
"Front the provider with a local HTTP/2 bridge and set providers.cursor.baseUrl to it.",
|
|
238
|
+
{ provider: "cursor", kind: "runtime", cause: error },
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return error;
|
|
242
|
+
}
|
|
243
|
+
|
|
216
244
|
function debugBytes(bytes: Uint8Array, asHex: boolean): string {
|
|
217
245
|
if (asHex) {
|
|
218
246
|
return Buffer.from(bytes).toString("hex");
|
|
@@ -431,7 +459,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
431
459
|
} else {
|
|
432
460
|
h2Client = http2.connect(baseUrl);
|
|
433
461
|
}
|
|
434
|
-
h2Client.on("error", error => settleH2(error));
|
|
462
|
+
h2Client.on("error", error => settleH2(mapH2TransportError(error, baseUrl)));
|
|
435
463
|
|
|
436
464
|
h2Request = h2Client.request(requestHeaders);
|
|
437
465
|
|
|
@@ -573,7 +601,8 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
573
601
|
});
|
|
574
602
|
|
|
575
603
|
h2Request.on("error", error => {
|
|
576
|
-
|
|
604
|
+
const mapped = mapH2TransportError(error, baseUrl);
|
|
605
|
+
void closeDebugLog().finally(() => settleH2(mapped));
|
|
577
606
|
});
|
|
578
607
|
|
|
579
608
|
if (options?.signal) {
|
package/src/providers/kimi.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* - OpenAI: https://api.kimi.com/coding/v1/chat/completions
|
|
6
6
|
* - Anthropic: https://api.kimi.com/coding/v1/messages
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Each discovered model selects its server-declared protocol; legacy models
|
|
9
|
+
* without protocol metadata retain the Anthropic-compatible default.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { getKimiCommonHeaders } from "../registry/oauth/kimi";
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
export type KimiApiFormat = OpenAIAnthropicApiFormat;
|
|
22
22
|
|
|
23
23
|
export interface KimiOptions extends OpenAIAnthropicShimOptions {
|
|
24
|
-
/** API format
|
|
24
|
+
/** Explicit API format override. Defaults to the model's discovered protocol. */
|
|
25
25
|
format?: KimiApiFormat;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -36,7 +36,8 @@ export function streamKimi(
|
|
|
36
36
|
): AssistantMessageEventStream {
|
|
37
37
|
return streamOpenAIAnthropicShim(model, context, options, {
|
|
38
38
|
anthropicBaseUrl: model.baseUrl.replace(/\/v1\/?$/, ""),
|
|
39
|
-
defaultFormat: "anthropic",
|
|
39
|
+
defaultFormat: model.compat.kimiApiFormat ?? "anthropic",
|
|
40
|
+
anthropicThinkingMode: model.compat.thinkingFormat === "kimi" ? "anthropic-adaptive" : undefined,
|
|
40
41
|
extraHeaders: getKimiCommonHeaders,
|
|
41
42
|
});
|
|
42
43
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
|
12
12
|
import { ANTHROPIC_THINKING, mapAnthropicToolChoice } from "../stream";
|
|
13
|
-
import type { Context, Model, ModelSpec, SimpleStreamOptions } from "../types";
|
|
13
|
+
import type { Context, Model, ModelSpec, SimpleStreamOptions, ThinkingControlMode } from "../types";
|
|
14
14
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
15
15
|
import { createProviderErrorMessage } from "./error-message";
|
|
16
16
|
import { streamAnthropic, streamOpenAICompletions } from "./register-builtins";
|
|
@@ -29,6 +29,8 @@ export interface OpenAIAnthropicShimConfig {
|
|
|
29
29
|
openaiBaseUrl?: string;
|
|
30
30
|
/** Default API format when caller does not specify one. */
|
|
31
31
|
defaultFormat: OpenAIAnthropicApiFormat;
|
|
32
|
+
/** Thinking transport used when this provider's Anthropic endpoint differs from generic budget semantics. */
|
|
33
|
+
anthropicThinkingMode?: ThinkingControlMode;
|
|
32
34
|
/** Provider-specific headers (e.g. auth/session) merged ahead of user-supplied headers. */
|
|
33
35
|
extraHeaders?: () => Record<string, string>;
|
|
34
36
|
}
|
|
@@ -67,6 +69,9 @@ export function streamOpenAIAnthropicShim(
|
|
|
67
69
|
contextWindow: model.contextWindow,
|
|
68
70
|
maxTokens: model.maxTokens,
|
|
69
71
|
reasoning: model.reasoning,
|
|
72
|
+
...(config.anthropicThinkingMode && model.thinking
|
|
73
|
+
? { thinking: { ...model.thinking, mode: config.anthropicThinkingMode } }
|
|
74
|
+
: {}),
|
|
70
75
|
input: model.input,
|
|
71
76
|
cost: model.cost,
|
|
72
77
|
} as ModelSpec<"anthropic-messages">);
|
|
@@ -95,6 +100,7 @@ export function streamOpenAIAnthropicShim(
|
|
|
95
100
|
fetch: options?.fetch,
|
|
96
101
|
thinkingEnabled,
|
|
97
102
|
thinkingBudgetTokens: thinkingBudget,
|
|
103
|
+
reasoning: config.anthropicThinkingMode ? reasoningEffort : undefined,
|
|
98
104
|
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
99
105
|
serviceTier: options?.serviceTier,
|
|
100
106
|
});
|
|
@@ -654,7 +654,7 @@ export type OpenAICompletionsParams = Omit<ChatCompletionCreateParamsStreaming,
|
|
|
654
654
|
top_k?: number;
|
|
655
655
|
min_p?: number;
|
|
656
656
|
repetition_penalty?: number;
|
|
657
|
-
thinking?: { type: "enabled" | "disabled"; keep?: "all" };
|
|
657
|
+
thinking?: { type: "enabled" | "disabled"; effort?: string; keep?: "all" };
|
|
658
658
|
enable_thinking?: boolean;
|
|
659
659
|
preserve_thinking?: boolean;
|
|
660
660
|
chat_template_kwargs?: { enable_thinking?: boolean; preserve_thinking?: boolean };
|
|
@@ -944,6 +944,11 @@ export function applyChatCompletionsCompatPolicy(params: OpenAICompletionsParams
|
|
|
944
944
|
encodeChatCompletionsDisabledReasoning(params, reasoning.disableMode);
|
|
945
945
|
return;
|
|
946
946
|
}
|
|
947
|
+
if (reasoning.dialect === "kimi" && reasoning.wireEffort !== undefined) {
|
|
948
|
+
params.thinking = { type: "enabled", effort: reasoning.wireEffort };
|
|
949
|
+
if (policy.compat.thinkingKeep) params.thinking.keep = policy.compat.thinkingKeep;
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
947
952
|
params.thinking = { type: "enabled" };
|
|
948
953
|
if (policy.compat.thinkingKeep) params.thinking.keep = policy.compat.thinkingKeep;
|
|
949
954
|
if (policy.compat.supportsReasoningEffort && reasoning.wireEffort !== undefined) {
|
|
@@ -1734,6 +1739,21 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
1734
1739
|
return outputItems;
|
|
1735
1740
|
}
|
|
1736
1741
|
|
|
1742
|
+
const syntheticToolImageMessages = new WeakSet<object>();
|
|
1743
|
+
|
|
1744
|
+
function insertResponsesToolOutput(messages: ResponseInput, output: ResponseInput[number]): void {
|
|
1745
|
+
let index = messages.length;
|
|
1746
|
+
while (index > 0) {
|
|
1747
|
+
const previous = messages[index - 1];
|
|
1748
|
+
if (typeof previous !== "object" || previous === null || !syntheticToolImageMessages.has(previous)) {
|
|
1749
|
+
break;
|
|
1750
|
+
}
|
|
1751
|
+
index -= 1;
|
|
1752
|
+
}
|
|
1753
|
+
messages.splice(index, 0, output);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/** Appends one tool result while keeping consecutive outputs ahead of its synthetic image messages. */
|
|
1737
1757
|
export function appendResponsesToolResultMessages<TApi extends Api>(
|
|
1738
1758
|
messages: ResponseInput,
|
|
1739
1759
|
toolResult: ToolResultMessage,
|
|
@@ -1780,13 +1800,13 @@ export function appendResponsesToolResultMessages<TApi extends Api>(
|
|
|
1780
1800
|
return;
|
|
1781
1801
|
}
|
|
1782
1802
|
if (supportsCustomToolCalls && customCallIds?.has(normalized.callId)) {
|
|
1783
|
-
messages
|
|
1803
|
+
insertResponsesToolOutput(messages, {
|
|
1784
1804
|
type: "custom_tool_call_output",
|
|
1785
1805
|
call_id: normalized.callId,
|
|
1786
1806
|
output,
|
|
1787
1807
|
} as ResponseInput[number]);
|
|
1788
1808
|
} else {
|
|
1789
|
-
messages
|
|
1809
|
+
insertResponsesToolOutput(messages, {
|
|
1790
1810
|
type: "function_call_output",
|
|
1791
1811
|
call_id: normalized.callId,
|
|
1792
1812
|
output,
|
|
@@ -1809,7 +1829,9 @@ export function appendResponsesToolResultMessages<TApi extends Api>(
|
|
|
1809
1829
|
} satisfies ResponseInputImage);
|
|
1810
1830
|
}
|
|
1811
1831
|
}
|
|
1812
|
-
|
|
1832
|
+
const imageMessage = { role: "user", content: contentParts } satisfies ResponseInput[number];
|
|
1833
|
+
syntheticToolImageMessages.add(imageMessage);
|
|
1834
|
+
messages.push(imageMessage);
|
|
1813
1835
|
}
|
|
1814
1836
|
|
|
1815
1837
|
/**
|
package/src/stream.ts
CHANGED
|
@@ -1178,12 +1178,17 @@ export function streamSimple<TApi extends Api>(
|
|
|
1178
1178
|
|
|
1179
1179
|
// Kimi Code - route to dedicated handler that wraps OpenAI or Anthropic API
|
|
1180
1180
|
if (isKimiModel(model)) {
|
|
1181
|
-
//
|
|
1182
|
-
|
|
1181
|
+
// streamKimi handles openai/anthropic format mapping internally, but the
|
|
1182
|
+
// mandatory-reasoning clamp is a request-shaping concern owned here: K3's
|
|
1183
|
+
// `supports_thinking_type: "only"` endpoint rejects disabled/omitted
|
|
1184
|
+
// thinking, so clamp disabled requests to the lowest supported effort
|
|
1185
|
+
// (mirrors the mapOptionsForApi path every other provider takes).
|
|
1186
|
+
const kimiOptions = normalizeMandatoryReasoningOptions(model, requestOptions);
|
|
1187
|
+
return withProviderInFlightLimit(model, kimiOptions, () =>
|
|
1183
1188
|
streamKimi(model as Model<"openai-completions">, context, {
|
|
1184
|
-
...
|
|
1189
|
+
...kimiOptions,
|
|
1185
1190
|
apiKey,
|
|
1186
|
-
format:
|
|
1191
|
+
format: kimiOptions?.kimiApiFormat,
|
|
1187
1192
|
}),
|
|
1188
1193
|
);
|
|
1189
1194
|
}
|
package/src/types.ts
CHANGED
|
@@ -539,7 +539,7 @@ export interface SimpleStreamOptions extends Omit<StreamOptions, "apiKey"> {
|
|
|
539
539
|
toolChoice?: ToolChoice;
|
|
540
540
|
/** OpenAI service tier for processing priority/cost control. Ignored by non-OpenAI providers. */
|
|
541
541
|
serviceTier?: ServiceTier;
|
|
542
|
-
/** API format
|
|
542
|
+
/** Explicit Kimi Code API format override; omitted uses live per-model protocol metadata. */
|
|
543
543
|
kimiApiFormat?: "openai" | "anthropic";
|
|
544
544
|
/** API format for Synthetic provider: "openai" or "anthropic" (default: "openai") */
|
|
545
545
|
syntheticApiFormat?: "openai" | "anthropic";
|
package/src/usage/claude.ts
CHANGED
|
@@ -96,11 +96,6 @@ interface ParsedApiLimitEntry {
|
|
|
96
96
|
displayName?: string;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
type ClaudeUsagePayload = {
|
|
100
|
-
payload: ClaudeUsageResponse;
|
|
101
|
-
orgId?: string;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
99
|
function parseIsoTime(value: string | undefined): number | undefined {
|
|
105
100
|
if (!value) return undefined;
|
|
106
101
|
const parsed = Date.parse(value);
|
|
@@ -178,22 +173,17 @@ function getNestedPayloadString(payload: Record<string, unknown>, key: string, n
|
|
|
178
173
|
return isRecord(nested) ? getPayloadString(nested, nestedKey) : undefined;
|
|
179
174
|
}
|
|
180
175
|
|
|
181
|
-
function extractUsageIdentity(payload: ClaudeUsageResponse
|
|
182
|
-
if (!isRecord(payload)) return {
|
|
176
|
+
function extractUsageIdentity(payload: ClaudeUsageResponse): { accountId?: string; email?: string } {
|
|
177
|
+
if (!isRecord(payload)) return {};
|
|
183
178
|
const accountId =
|
|
184
179
|
getPayloadString(payload, "account_id") ??
|
|
185
180
|
getPayloadString(payload, "accountId") ??
|
|
186
181
|
getPayloadString(payload, "user_id") ??
|
|
187
182
|
getPayloadString(payload, "userId") ??
|
|
188
|
-
getPayloadString(payload, "org_id") ??
|
|
189
|
-
getPayloadString(payload, "orgId") ??
|
|
190
183
|
getNestedPayloadString(payload, "account", "uuid") ??
|
|
191
184
|
getNestedPayloadString(payload, "account", "id") ??
|
|
192
|
-
getNestedPayloadString(payload, "organization", "uuid") ??
|
|
193
|
-
getNestedPayloadString(payload, "organization", "id") ??
|
|
194
185
|
getNestedPayloadString(payload, "user", "uuid") ??
|
|
195
|
-
getNestedPayloadString(payload, "user", "id")
|
|
196
|
-
orgId;
|
|
186
|
+
getNestedPayloadString(payload, "user", "id");
|
|
197
187
|
const email =
|
|
198
188
|
getPayloadString(payload, "email") ??
|
|
199
189
|
getPayloadString(payload, "user_email") ??
|
|
@@ -263,16 +253,13 @@ async function fetchUsagePayload(
|
|
|
263
253
|
headers: Record<string, string>,
|
|
264
254
|
ctx: UsageFetchContext,
|
|
265
255
|
signal?: AbortSignal,
|
|
266
|
-
): Promise<
|
|
256
|
+
): Promise<ClaudeUsageResponse | null> {
|
|
267
257
|
if (signal?.aborted) return null;
|
|
268
258
|
|
|
269
259
|
let lastPayload: ClaudeUsageResponse | null = null;
|
|
270
|
-
let lastOrgId: string | undefined;
|
|
271
260
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
272
261
|
try {
|
|
273
262
|
const response = await ctx.fetch(url, { headers, signal });
|
|
274
|
-
const orgId = response.headers.get("anthropic-organization-id")?.trim() || undefined;
|
|
275
|
-
lastOrgId = orgId ?? lastOrgId;
|
|
276
263
|
|
|
277
264
|
if (!response.ok) {
|
|
278
265
|
const retryable = isRetryableStatus(response.status);
|
|
@@ -292,7 +279,7 @@ async function fetchUsagePayload(
|
|
|
292
279
|
if (isRecord(parsed)) {
|
|
293
280
|
const payload = parsed as ClaudeUsageResponse;
|
|
294
281
|
lastPayload = payload;
|
|
295
|
-
if (hasUsageData(payload)) return
|
|
282
|
+
if (hasUsageData(payload)) return payload;
|
|
296
283
|
}
|
|
297
284
|
|
|
298
285
|
ctx.logger?.warn("Claude usage response missing usage data", {
|
|
@@ -311,7 +298,7 @@ async function fetchUsagePayload(
|
|
|
311
298
|
}
|
|
312
299
|
}
|
|
313
300
|
|
|
314
|
-
return lastPayload
|
|
301
|
+
return lastPayload;
|
|
315
302
|
}
|
|
316
303
|
|
|
317
304
|
interface ClaudeProfile {
|
|
@@ -507,9 +494,8 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
|
|
507
494
|
authorization: `Bearer ${credential.accessToken}`,
|
|
508
495
|
};
|
|
509
496
|
|
|
510
|
-
const
|
|
511
|
-
if (!
|
|
512
|
-
const { payload, orgId } = payloadResult;
|
|
497
|
+
const payload = await fetchUsagePayload(url, headers, ctx, params.signal);
|
|
498
|
+
if (!payload || !isRecord(payload)) return null;
|
|
513
499
|
|
|
514
500
|
const apiLimitEntries = parseApiLimitEntries(payload.limits);
|
|
515
501
|
const fiveHour = parseBucket(payload.five_hour) ?? apiLimitEntries.find(entry => entry.kind === "session")?.bucket;
|
|
@@ -563,7 +549,7 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
|
|
563
549
|
].filter((limit): limit is UsageLimit => limit !== null);
|
|
564
550
|
|
|
565
551
|
if (limits.length === 0) return null;
|
|
566
|
-
const identity = extractUsageIdentity(payload
|
|
552
|
+
const identity = extractUsageIdentity(payload);
|
|
567
553
|
let accountId = identity.accountId ?? credential.accountId;
|
|
568
554
|
let email = identity.email ?? credential.email;
|
|
569
555
|
if ((!accountId || !email) && !params.signal?.aborted) {
|
|
@@ -580,7 +566,7 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
|
|
580
566
|
endpoint: url,
|
|
581
567
|
...(accountId ? { accountId } : {}),
|
|
582
568
|
...(email ? { email } : {}),
|
|
583
|
-
...(orgId ? { orgId } : {}),
|
|
569
|
+
...(credential.orgId ? { orgId: credential.orgId } : {}),
|
|
584
570
|
},
|
|
585
571
|
raw: payload,
|
|
586
572
|
};
|
|
@@ -195,3 +195,8 @@ export class AssistantMessageEventStream extends EventStream<AssistantMessageEve
|
|
|
195
195
|
this.endWaiting();
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
+
|
|
199
|
+
/** Create an assistant-message event stream for legacy extension providers. */
|
|
200
|
+
export function createAssistantMessageEventStream(): AssistantMessageEventStream {
|
|
201
|
+
return new AssistantMessageEventStream();
|
|
202
|
+
}
|