@oh-my-pi/pi-ai 16.2.5 → 16.2.7
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 +28 -0
- package/dist/types/auth-storage.d.ts +11 -10
- package/dist/types/providers/__tests__/kimi-code-thinking.test.d.ts +1 -0
- package/dist/types/providers/google-auth.d.ts +8 -0
- package/dist/types/providers/google-interactions.d.ts +65 -0
- package/dist/types/providers/google-shared.d.ts +20 -5
- package/dist/types/providers/google-types.d.ts +5 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +1 -1
- package/dist/types/providers/openai-shared.d.ts +3 -3
- package/dist/types/types.d.ts +80 -30
- package/dist/types/utils/leaked-thinking-stream.d.ts +29 -0
- package/package.json +4 -4
- package/src/auth-storage.ts +56 -52
- package/src/providers/__tests__/kimi-code-thinking.test.ts +112 -0
- package/src/providers/anthropic.ts +11 -10
- package/src/providers/google-auth.ts +25 -0
- package/src/providers/google-gemini-cli.ts +90 -41
- package/src/providers/google-interactions.ts +753 -0
- package/src/providers/google-shared.ts +74 -13
- package/src/providers/google-types.ts +5 -1
- package/src/providers/google-vertex.ts +117 -26
- package/src/providers/google.ts +61 -19
- package/src/providers/openai-chat-server.ts +2 -2
- package/src/providers/openai-codex/request-transformer.ts +17 -4
- package/src/providers/openai-codex-responses.ts +23 -5
- package/src/providers/openai-shared.ts +5 -11
- package/src/stream.ts +34 -12
- package/src/types.ts +164 -51
- package/src/usage/google-antigravity.ts +59 -5
- package/src/utils/leaked-thinking-stream.ts +260 -0
package/src/auth-storage.ts
CHANGED
|
@@ -1736,8 +1736,8 @@ export class AuthStorage {
|
|
|
1736
1736
|
/**
|
|
1737
1737
|
* Classify where a provider's auth comes from, following the same precedence
|
|
1738
1738
|
* as {@link AuthStorage.getApiKey}: runtime override → config override →
|
|
1739
|
-
* stored
|
|
1740
|
-
*
|
|
1739
|
+
* stored OAuth → env var → stored api_key → fallback resolver. Returns
|
|
1740
|
+
* undefined when no auth is configured.
|
|
1741
1741
|
*
|
|
1742
1742
|
* Compact, structured counterpart to {@link describeCredentialSource}.
|
|
1743
1743
|
*/
|
|
@@ -1745,10 +1745,9 @@ export class AuthStorage {
|
|
|
1745
1745
|
if (this.#runtimeOverrides.has(provider)) return { kind: "runtime" };
|
|
1746
1746
|
if (this.#configOverrides.has(provider)) return { kind: "config" };
|
|
1747
1747
|
const stored = this.#getCredentialsForProvider(provider);
|
|
1748
|
-
if (stored.
|
|
1749
|
-
return { kind: stored.some(credential => credential.type === "api_key") ? "api_key" : "oauth" };
|
|
1750
|
-
}
|
|
1748
|
+
if (stored.some(credential => credential.type === "oauth")) return { kind: "oauth" };
|
|
1751
1749
|
if (getEnvApiKey(provider)) return { kind: "env", envVar: getEnvApiKeyName(provider) };
|
|
1750
|
+
if (stored.some(credential => credential.type === "api_key")) return { kind: "api_key" };
|
|
1752
1751
|
if (this.#fallbackResolver?.(provider)) return { kind: "fallback" };
|
|
1753
1752
|
return undefined;
|
|
1754
1753
|
}
|
|
@@ -3760,12 +3759,8 @@ export class AuthStorage {
|
|
|
3760
3759
|
return configKey;
|
|
3761
3760
|
}
|
|
3762
3761
|
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
return this.#configValueResolver(apiKeySelection.credential.key);
|
|
3766
|
-
}
|
|
3767
|
-
|
|
3768
|
-
// Return current OAuth access token only if it is not already expired.
|
|
3762
|
+
// Precedence: a deliberate OAuth login wins, then an explicit env var, then a stored
|
|
3763
|
+
// static api_key (which may be a stale broker-migrated copy) as a last resort.
|
|
3769
3764
|
const oauthSelection = this.#selectCredentialByType(provider, "oauth");
|
|
3770
3765
|
if (oauthSelection) {
|
|
3771
3766
|
const expiresAt = oauthSelection.credential.expires;
|
|
@@ -3784,17 +3779,22 @@ export class AuthStorage {
|
|
|
3784
3779
|
const envKey = getEnvApiKey(provider);
|
|
3785
3780
|
if (envKey) return envKey;
|
|
3786
3781
|
|
|
3782
|
+
const apiKeySelection = this.#selectCredentialByType(provider, "api_key");
|
|
3783
|
+
if (apiKeySelection) {
|
|
3784
|
+
return this.#configValueResolver(apiKeySelection.credential.key);
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
3787
|
return this.#fallbackResolver?.(provider) ?? undefined;
|
|
3788
3788
|
}
|
|
3789
3789
|
|
|
3790
3790
|
/**
|
|
3791
3791
|
* Get API key for a provider.
|
|
3792
|
-
* Priority:
|
|
3792
|
+
* Priority (first match wins):
|
|
3793
3793
|
* 1. Runtime override (CLI --api-key)
|
|
3794
3794
|
* 2. Config override (models.yml `providers.<name>.apiKey`)
|
|
3795
|
-
* 3.
|
|
3796
|
-
* 4.
|
|
3797
|
-
* 5.
|
|
3795
|
+
* 3. OAuth token from storage (auto-refreshed)
|
|
3796
|
+
* 4. Environment variable
|
|
3797
|
+
* 5. Stored API key (e.g. a broker-migrated copy) — last resort, so an explicit env var wins
|
|
3798
3798
|
* 6. Fallback resolver (models.yml custom providers, last-resort)
|
|
3799
3799
|
*/
|
|
3800
3800
|
async getApiKey(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined> {
|
|
@@ -3814,25 +3814,27 @@ export class AuthStorage {
|
|
|
3814
3814
|
return configKey;
|
|
3815
3815
|
}
|
|
3816
3816
|
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
|
|
3820
|
-
return this.#configValueResolver(apiKeySelection.credential.key);
|
|
3821
|
-
}
|
|
3822
|
-
|
|
3817
|
+
// Precedence: a deliberate OAuth login wins, then an explicit env var, then a stored
|
|
3818
|
+
// static api_key (which may be a stale broker-migrated copy) as a last resort.
|
|
3823
3819
|
const oauthResolved = await this.#resolveOAuthSelection(provider, sessionId, options);
|
|
3824
3820
|
if (oauthResolved) {
|
|
3825
3821
|
return oauthResolved.apiKey;
|
|
3826
3822
|
}
|
|
3827
3823
|
|
|
3828
|
-
//
|
|
3829
|
-
//
|
|
3830
|
-
//
|
|
3831
|
-
// getOAuthAccountId() correctly suppresses account_uuid for this session.
|
|
3824
|
+
// Past OAuth: the session sticky (if any) is stale — the request authenticates via
|
|
3825
|
+
// env/api_key/fallback, not OAuth, so clear it now so getOAuthAccountId() correctly
|
|
3826
|
+
// suppresses account_uuid for this session.
|
|
3832
3827
|
if (sessionId) this.#sessionLastCredential.get(provider)?.delete(sessionId);
|
|
3828
|
+
|
|
3833
3829
|
const envKey = getEnvApiKey(provider);
|
|
3834
3830
|
if (envKey) return envKey;
|
|
3835
3831
|
|
|
3832
|
+
const apiKeySelection = this.#selectCredentialByType(provider, "api_key", sessionId);
|
|
3833
|
+
if (apiKeySelection) {
|
|
3834
|
+
this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
|
|
3835
|
+
return this.#configValueResolver(apiKeySelection.credential.key);
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3836
3838
|
// Fall back to custom resolver (e.g., models.json custom providers)
|
|
3837
3839
|
return this.#fallbackResolver?.(provider) ?? undefined;
|
|
3838
3840
|
}
|
|
@@ -4486,12 +4488,13 @@ export class AuthStorage {
|
|
|
4486
4488
|
/**
|
|
4487
4489
|
* Describe where the active credential for a provider came from.
|
|
4488
4490
|
*
|
|
4489
|
-
*
|
|
4491
|
+
* Mirrors {@link AuthStorage.getApiKey} precedence, highest first:
|
|
4490
4492
|
* 1. Runtime override (`--api-key`).
|
|
4491
4493
|
* 2. Config override (`models.yml` `providers.<name>.apiKey`).
|
|
4492
|
-
* 3. Stored credential
|
|
4493
|
-
*
|
|
4494
|
-
*
|
|
4494
|
+
* 3. Stored OAuth credential.
|
|
4495
|
+
* 4. Env var — overrides a stored static api_key (e.g. a stale broker copy).
|
|
4496
|
+
* 5. Stored api_key credential.
|
|
4497
|
+
* 6. Fallback resolver.
|
|
4495
4498
|
*
|
|
4496
4499
|
* The string is purely informational; consumers must not parse it.
|
|
4497
4500
|
*/
|
|
@@ -4505,30 +4508,31 @@ export class AuthStorage {
|
|
|
4505
4508
|
|
|
4506
4509
|
const baseLabel = this.#sourceLabel ?? "local store";
|
|
4507
4510
|
const stored = this.#getStoredCredentials(provider);
|
|
4508
|
-
if (stored.length === 0) {
|
|
4509
|
-
if (getEnvApiKey(provider)) return `env ${baseLabel ? `(fallback over ${baseLabel})` : ""}`.trim();
|
|
4510
|
-
if (this.#fallbackResolver?.(provider) !== undefined) return `fallback resolver`;
|
|
4511
|
-
return undefined;
|
|
4512
|
-
}
|
|
4513
|
-
|
|
4514
4511
|
const session = sessionId ? this.#sessionLastCredential.get(provider)?.get(sessionId) : undefined;
|
|
4515
|
-
//
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4512
|
+
// Describe the stored credential of a given type, honoring the session sticky index.
|
|
4513
|
+
const describeStored = (type: AuthCredential["type"]): string | undefined => {
|
|
4514
|
+
const typed = stored
|
|
4515
|
+
.map((entry, index) => ({ entry, index }))
|
|
4516
|
+
.filter(({ entry }) => entry.credential.type === type);
|
|
4517
|
+
if (typed.length === 0) return undefined;
|
|
4518
|
+
const index = session?.type === type ? session.index : typed[0].index;
|
|
4519
|
+
const chosen = stored[index] ?? typed[0].entry;
|
|
4520
|
+
const credential = chosen.credential;
|
|
4521
|
+
const identity =
|
|
4522
|
+
credential.type === "oauth"
|
|
4523
|
+
? (credential.email ?? credential.accountId ?? credential.projectId ?? `cred ${chosen.id}`)
|
|
4524
|
+
: `cred ${chosen.id}`;
|
|
4525
|
+
return `${baseLabel} · ${type} #${chosen.id} (${identity})`;
|
|
4526
|
+
};
|
|
4527
|
+
|
|
4528
|
+
// A deliberate OAuth login wins; then an explicit env var; then a stored static api_key.
|
|
4529
|
+
const oauthSource = describeStored("oauth");
|
|
4530
|
+
if (oauthSource) return oauthSource;
|
|
4531
|
+
if (getEnvApiKey(provider)) return `env (over ${baseLabel})`;
|
|
4532
|
+
const apiKeySource = describeStored("api_key");
|
|
4533
|
+
if (apiKeySource) return apiKeySource;
|
|
4534
|
+
if (this.#fallbackResolver?.(provider) !== undefined) return "fallback resolver";
|
|
4535
|
+
return undefined;
|
|
4532
4536
|
}
|
|
4533
4537
|
}
|
|
4534
4538
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { getBundledModel } from "@oh-my-pi/pi-catalog";
|
|
3
|
+
import type { Context } from "../../types";
|
|
4
|
+
import type { MessageCreateParamsStreaming } from "../anthropic-wire";
|
|
5
|
+
import { streamOpenAIAnthropicShim } from "../openai-anthropic-shim";
|
|
6
|
+
import {
|
|
7
|
+
applyChatCompletionsCompatPolicy,
|
|
8
|
+
type OpenAICompletionsParams,
|
|
9
|
+
resolveOpenAICompatPolicy,
|
|
10
|
+
} from "../openai-shared";
|
|
11
|
+
|
|
12
|
+
const BASE_CHAT_COMPLETIONS_PARAMS: OpenAICompletionsParams = { messages: [], model: "unused", stream: true };
|
|
13
|
+
const TITLE_CONTEXT: Context = {
|
|
14
|
+
systemPrompt: ["Generate a title."],
|
|
15
|
+
messages: [{ role: "user", content: "Explain the login failure", timestamp: 0 }],
|
|
16
|
+
tools: [
|
|
17
|
+
{
|
|
18
|
+
name: "set_title",
|
|
19
|
+
description: "Set title",
|
|
20
|
+
parameters: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: { title: { type: "string" } },
|
|
23
|
+
required: ["title"],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe("Kimi K2.7 Code thinking policy", () => {
|
|
31
|
+
it("omits disabled thinking for title-generator-style Kimi Code requests", () => {
|
|
32
|
+
const model = getBundledModel<"openai-completions">("kimi-code", "kimi-for-coding");
|
|
33
|
+
const policy = resolveOpenAICompatPolicy(model, {
|
|
34
|
+
endpoint: "chat-completions",
|
|
35
|
+
disableReasoning: true,
|
|
36
|
+
toolChoice: { type: "tool", name: "set_title" },
|
|
37
|
+
});
|
|
38
|
+
const params = { ...BASE_CHAT_COMPLETIONS_PARAMS };
|
|
39
|
+
|
|
40
|
+
applyChatCompletionsCompatPolicy(params, policy);
|
|
41
|
+
|
|
42
|
+
expect("thinking" in params).toBe(false);
|
|
43
|
+
expect(model.compat.supportsForcedToolChoice).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("enables thinking and downgrades forced tool choice on Kimi Code's Anthropic endpoint", async () => {
|
|
47
|
+
const model = getBundledModel<"openai-completions">("kimi-code", "kimi-for-coding");
|
|
48
|
+
let payload: MessageCreateParamsStreaming | undefined;
|
|
49
|
+
const stream = streamOpenAIAnthropicShim(
|
|
50
|
+
model,
|
|
51
|
+
TITLE_CONTEXT,
|
|
52
|
+
{
|
|
53
|
+
apiKey: "test-key",
|
|
54
|
+
maxTokens: 1024,
|
|
55
|
+
disableReasoning: true,
|
|
56
|
+
toolChoice: { type: "tool", name: "set_title" },
|
|
57
|
+
onPayload: body => {
|
|
58
|
+
payload = body as MessageCreateParamsStreaming;
|
|
59
|
+
throw new Error("stop after payload capture");
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
anthropicBaseUrl: "https://api.kimi.com/coding",
|
|
64
|
+
defaultFormat: "anthropic",
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await stream.result();
|
|
69
|
+
|
|
70
|
+
expect(payload?.thinking?.type).toBe("enabled");
|
|
71
|
+
expect(payload?.tool_choice).toEqual({ type: "auto" });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("omits disabled thinking for native Moonshot Kimi K2.7 Code variants", () => {
|
|
75
|
+
for (const modelId of ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"]) {
|
|
76
|
+
const model = getBundledModel<"openai-completions">("moonshot", modelId);
|
|
77
|
+
const policy = resolveOpenAICompatPolicy(model, {
|
|
78
|
+
endpoint: "chat-completions",
|
|
79
|
+
disableReasoning: true,
|
|
80
|
+
});
|
|
81
|
+
const params = { ...BASE_CHAT_COMPLETIONS_PARAMS };
|
|
82
|
+
applyChatCompletionsCompatPolicy(params, policy);
|
|
83
|
+
|
|
84
|
+
expect("thinking" in params).toBe(false);
|
|
85
|
+
expect(model.compat.supportsForcedToolChoice).toBe(false);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("keeps the openai disable shape for non-native Kimi K2.7 Code aliases", () => {
|
|
90
|
+
for (const { provider, id } of [
|
|
91
|
+
{ provider: "fireworks", id: "kimi-k2.7-code" },
|
|
92
|
+
{ provider: "openrouter", id: "moonshotai/kimi-k2.7-code" },
|
|
93
|
+
] as const) {
|
|
94
|
+
const model = getBundledModel<"openai-completions">(provider, id);
|
|
95
|
+
expect(model.compat.supportsForcedToolChoice).toBe(true);
|
|
96
|
+
expect(model.compat.reasoningDisableMode).not.toBe("omit");
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("keeps explicit disabled thinking for Kimi K2.6", () => {
|
|
101
|
+
const model = getBundledModel<"openai-completions">("moonshot", "kimi-k2.6");
|
|
102
|
+
const policy = resolveOpenAICompatPolicy(model, {
|
|
103
|
+
endpoint: "chat-completions",
|
|
104
|
+
disableReasoning: true,
|
|
105
|
+
});
|
|
106
|
+
const params = { ...BASE_CHAT_COMPLETIONS_PARAMS };
|
|
107
|
+
|
|
108
|
+
applyChatCompletionsCompatPolicy(params, policy);
|
|
109
|
+
|
|
110
|
+
expect(params.thinking).toEqual({ type: "disabled" });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -43,7 +43,6 @@ import type {
|
|
|
43
43
|
ToolResultMessage,
|
|
44
44
|
Usage,
|
|
45
45
|
} from "../types";
|
|
46
|
-
import { resolveServiceTier } from "../types";
|
|
47
46
|
import { isRecord, normalizeSystemPrompts, normalizeToolCallId, resolveCacheRetention } from "../utils";
|
|
48
47
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
49
48
|
import {
|
|
@@ -1595,7 +1594,7 @@ const streamAnthropicOnce = (
|
|
|
1595
1594
|
isOAuthToken = false;
|
|
1596
1595
|
} else {
|
|
1597
1596
|
const extraBetas = normalizeExtraBetas(options?.betas);
|
|
1598
|
-
const wantsAnthropicPriority =
|
|
1597
|
+
const wantsAnthropicPriority = model.provider === "anthropic" && options?.serviceTier === "priority";
|
|
1599
1598
|
// Skip the fast-mode beta when this session already learned the
|
|
1600
1599
|
// endpoint+model rejects fast mode; `speed` is dropped from the params
|
|
1601
1600
|
// too (dropFastMode), so the request stays a faithful non-fast request.
|
|
@@ -2191,7 +2190,8 @@ const streamAnthropicOnce = (
|
|
|
2191
2190
|
}
|
|
2192
2191
|
if (
|
|
2193
2192
|
!dropFastMode &&
|
|
2194
|
-
|
|
2193
|
+
model.provider === "anthropic" &&
|
|
2194
|
+
options?.serviceTier === "priority" &&
|
|
2195
2195
|
firstTokenTime === undefined &&
|
|
2196
2196
|
AIError.isFastModeUnsupported(streamFailure)
|
|
2197
2197
|
) {
|
|
@@ -2259,7 +2259,7 @@ const streamAnthropicOnce = (
|
|
|
2259
2259
|
}
|
|
2260
2260
|
output.duration = performance.now() - startTime;
|
|
2261
2261
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
2262
|
-
if (dropFastMode &&
|
|
2262
|
+
if (dropFastMode && model.provider === "anthropic" && options?.serviceTier === "priority") {
|
|
2263
2263
|
output.disabledFeatures = [...(output.disabledFeatures ?? []), "priority"];
|
|
2264
2264
|
}
|
|
2265
2265
|
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
@@ -2876,9 +2876,10 @@ function buildParams(
|
|
|
2876
2876
|
let thinking: MessageCreateParamsStreaming["thinking"] | undefined;
|
|
2877
2877
|
let outputConfigEffort: AnthropicOutputEffort | undefined;
|
|
2878
2878
|
if (model.reasoning) {
|
|
2879
|
-
if (options?.thinkingEnabled) {
|
|
2879
|
+
if (options?.thinkingEnabled || model.compat.requiresThinkingEnabled) {
|
|
2880
|
+
const thinkingOptions = options ?? {};
|
|
2880
2881
|
const mode = model.thinking?.mode;
|
|
2881
|
-
const effort = resolveAnthropicAdaptiveEffort(model,
|
|
2882
|
+
const effort = resolveAnthropicAdaptiveEffort(model, thinkingOptions);
|
|
2882
2883
|
const compat = model.compat;
|
|
2883
2884
|
if (mode === "anthropic-adaptive" && !compat.disableAdaptiveThinking) {
|
|
2884
2885
|
const adaptive: { type: "adaptive"; display?: AnthropicThinkingDisplay } = { type: "adaptive" };
|
|
@@ -2889,15 +2890,15 @@ function buildParams(
|
|
|
2889
2890
|
// support: Opus 4.6 / Sonnet 4.6+ reject it with a 400, so an explicit
|
|
2890
2891
|
// `thinkingDisplay` MUST NOT force it onto a model that can't accept it.
|
|
2891
2892
|
if (model.thinking?.supportsDisplay) {
|
|
2892
|
-
adaptive.display =
|
|
2893
|
+
adaptive.display = thinkingOptions.thinkingDisplay ?? "summarized";
|
|
2893
2894
|
}
|
|
2894
2895
|
thinking = adaptive;
|
|
2895
2896
|
if (effort && effort !== "adaptive") outputConfigEffort = effort;
|
|
2896
2897
|
} else {
|
|
2897
2898
|
thinking = {
|
|
2898
2899
|
type: "enabled",
|
|
2899
|
-
budget_tokens:
|
|
2900
|
-
display:
|
|
2900
|
+
budget_tokens: thinkingOptions.thinkingBudgetTokens || 1024,
|
|
2901
|
+
display: thinkingOptions.thinkingDisplay ?? "summarized",
|
|
2901
2902
|
};
|
|
2902
2903
|
if (mode === "anthropic-budget-effort" && effort && effort !== "adaptive") outputConfigEffort = effort;
|
|
2903
2904
|
}
|
|
@@ -2996,7 +2997,7 @@ function buildParams(
|
|
|
2996
2997
|
seqs.length > ANTHROPIC_STOP_SEQUENCES_MAX ? seqs.slice(0, ANTHROPIC_STOP_SEQUENCES_MAX) : seqs;
|
|
2997
2998
|
}
|
|
2998
2999
|
|
|
2999
|
-
if (
|
|
3000
|
+
if (model.provider === "anthropic" && options?.serviceTier === "priority") {
|
|
3000
3001
|
params.speed = "fast";
|
|
3001
3002
|
}
|
|
3002
3003
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { Buffer } from "node:buffer";
|
|
16
|
+
import * as fs from "node:fs";
|
|
16
17
|
import * as os from "node:os";
|
|
17
18
|
import * as path from "node:path";
|
|
18
19
|
import { $envpos, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
@@ -281,6 +282,11 @@ const SHARED_TOKEN_RESOLVE_TIMEOUT_MS = 30_000;
|
|
|
281
282
|
* The token is cached in module scope and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` ms before it expires.
|
|
282
283
|
*/
|
|
283
284
|
export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl }): Promise<string> {
|
|
285
|
+
// An explicit access token (e.g. `gcloud auth print-access-token`) bypasses the cache so a
|
|
286
|
+
// refreshed env token takes effect immediately. `CLOUDSDK_AUTH_ACCESS_TOKEN` is gcloud's own
|
|
287
|
+
// override var; `GOOGLE_CLOUD_ACCESS_TOKEN` is the omp-facing alias.
|
|
288
|
+
const explicitToken = Bun.env.GOOGLE_CLOUD_ACCESS_TOKEN || Bun.env.CLOUDSDK_AUTH_ACCESS_TOKEN;
|
|
289
|
+
if (explicitToken) return explicitToken;
|
|
284
290
|
const fetchImpl = options?.fetch ?? globalThis.fetch.bind(globalThis);
|
|
285
291
|
const skew = getRefreshSkewMs();
|
|
286
292
|
const now = Date.now();
|
|
@@ -323,3 +329,22 @@ export function __resetVertexTokenCache(): void {
|
|
|
323
329
|
tokenCache.clear();
|
|
324
330
|
inflight.clear();
|
|
325
331
|
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Sync best-effort probe for a usable Vertex bearer credential source — an explicit access-token
|
|
335
|
+
* env var, `GOOGLE_APPLICATION_CREDENTIALS`, a user ADC file, or a GCP runtime whose metadata
|
|
336
|
+
* server can mint ADC (GCE/Cloud Run/App Engine/Functions). Lets callers prefer the bearer
|
|
337
|
+
* Interactions transport only when ADC is actually reachable, without paying the async
|
|
338
|
+
* metadata-probe cost for API-key-only setups.
|
|
339
|
+
*/
|
|
340
|
+
export function hasVertexBearerCredentialsHint(): boolean {
|
|
341
|
+
if (Bun.env.GOOGLE_CLOUD_ACCESS_TOKEN || Bun.env.CLOUDSDK_AUTH_ACCESS_TOKEN) return true;
|
|
342
|
+
if (Bun.env.GOOGLE_APPLICATION_CREDENTIALS) return true;
|
|
343
|
+
// GCP-hosted runtimes expose ADC via the metadata server; these env vars mark those runtimes.
|
|
344
|
+
if (Bun.env.K_SERVICE || Bun.env.FUNCTION_TARGET || Bun.env.GAE_ENV || Bun.env.GCE_METADATA_HOST) return true;
|
|
345
|
+
try {
|
|
346
|
+
return fs.existsSync(userAdcPath());
|
|
347
|
+
} catch {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -35,6 +35,7 @@ import { armPreResponseTimeout, getStreamFirstEventTimeoutMs } from "../utils/id
|
|
|
35
35
|
// Refresh is the sole responsibility of AuthStorage (broker-aware, single-flighted);
|
|
36
36
|
// the stream provider trusts the access token threaded through `options.apiKey`.
|
|
37
37
|
import { normalizeSchemaForCCA } from "../utils/schema";
|
|
38
|
+
import { StreamMarkupHealing, type StreamMarkupHealingEvent } from "../utils/stream-markup-healing";
|
|
38
39
|
import type { Content, FunctionCallingConfigMode, ThinkingConfig } from "./google-shared";
|
|
39
40
|
import {
|
|
40
41
|
convertMessages,
|
|
@@ -665,15 +666,43 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
665
666
|
let currentBlock: TextContent | ThinkingContent | null = null;
|
|
666
667
|
const blocks = output.content;
|
|
667
668
|
const blockIndex = () => blocks.length - 1;
|
|
669
|
+
const visibleTextHealing = new StreamMarkupHealing({ pattern: "thinking" });
|
|
668
670
|
|
|
669
671
|
let isBuffering = false;
|
|
670
672
|
let textBuffer = "";
|
|
671
673
|
let bufferedTextSignature: string | undefined;
|
|
672
674
|
|
|
673
|
-
const
|
|
674
|
-
if (!
|
|
675
|
-
currentBlock
|
|
676
|
-
currentBlock
|
|
675
|
+
const endCurrentBlock = (): void => {
|
|
676
|
+
if (!currentBlock) return;
|
|
677
|
+
pushBlockEndEvent(currentBlock, blockIndex(), output, stream);
|
|
678
|
+
currentBlock = null;
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const startTextBlock = (): TextContent => {
|
|
682
|
+
let block = currentBlock;
|
|
683
|
+
if (block?.type !== "text") {
|
|
684
|
+
endCurrentBlock();
|
|
685
|
+
block = startTextOrThinkingBlock(false, output, stream, ensureStarted);
|
|
686
|
+
currentBlock = block;
|
|
687
|
+
}
|
|
688
|
+
return block;
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const startThinkingBlock = (): ThinkingContent => {
|
|
692
|
+
let block = currentBlock;
|
|
693
|
+
if (block?.type !== "thinking") {
|
|
694
|
+
endCurrentBlock();
|
|
695
|
+
block = startTextOrThinkingBlock(true, output, stream, ensureStarted);
|
|
696
|
+
currentBlock = block;
|
|
697
|
+
}
|
|
698
|
+
return block;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const emitVisibleText = (delta: string, thoughtSignature?: string): void => {
|
|
702
|
+
if (!delta) return;
|
|
703
|
+
const block = startTextBlock();
|
|
704
|
+
block.text += delta;
|
|
705
|
+
block.textSignature = retainThoughtSignature(block.textSignature, thoughtSignature);
|
|
677
706
|
stream.push({
|
|
678
707
|
type: "text_delta",
|
|
679
708
|
contentIndex: blockIndex(),
|
|
@@ -682,6 +711,48 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
682
711
|
});
|
|
683
712
|
};
|
|
684
713
|
|
|
714
|
+
const emitVisibleThinking = (delta: string): void => {
|
|
715
|
+
if (!delta) return;
|
|
716
|
+
const block = startThinkingBlock();
|
|
717
|
+
block.thinking += delta;
|
|
718
|
+
stream.push({
|
|
719
|
+
type: "thinking_delta",
|
|
720
|
+
contentIndex: blockIndex(),
|
|
721
|
+
delta,
|
|
722
|
+
partial: output,
|
|
723
|
+
});
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const emitHealingEvent = (event: StreamMarkupHealingEvent, thoughtSignature?: string): void => {
|
|
727
|
+
if (event.type === "text") {
|
|
728
|
+
emitVisibleText(event.text, thoughtSignature);
|
|
729
|
+
} else if (event.type === "thinking") {
|
|
730
|
+
emitVisibleThinking(event.thinking);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const feedVisibleText = (delta: string, thoughtSignature?: string): void => {
|
|
735
|
+
for (const event of visibleTextHealing.feedEvents(delta)) {
|
|
736
|
+
emitHealingEvent(event, thoughtSignature);
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
const flushVisibleText = (thoughtSignature?: string): void => {
|
|
741
|
+
for (const event of visibleTextHealing.flushEvents()) {
|
|
742
|
+
emitHealingEvent(event, thoughtSignature);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const retainCurrentBlockThoughtSignature = (thoughtSignature: string): void => {
|
|
747
|
+
const block = currentBlock;
|
|
748
|
+
if (!block) return;
|
|
749
|
+
if (block.type === "thinking") {
|
|
750
|
+
block.thinkingSignature = retainThoughtSignature(block.thinkingSignature, thoughtSignature);
|
|
751
|
+
} else {
|
|
752
|
+
block.textSignature = retainThoughtSignature(block.textSignature, thoughtSignature);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
|
|
685
756
|
for await (const chunk of readSseJson<CloudCodeAssistResponseChunk>(
|
|
686
757
|
activeResponse.body!,
|
|
687
758
|
options?.signal,
|
|
@@ -710,20 +781,12 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
710
781
|
for (const part of candidate.content.parts) {
|
|
711
782
|
if (part.text !== undefined && part.text !== "") {
|
|
712
783
|
const isThinking = isThinkingPart(part);
|
|
713
|
-
if (
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
pushBlockEndEvent(currentBlock, blockIndex(), output, stream);
|
|
720
|
-
}
|
|
721
|
-
currentBlock = startTextOrThinkingBlock(isThinking, output, stream, ensureStarted);
|
|
722
|
-
}
|
|
723
|
-
if (currentBlock.type === "thinking") {
|
|
724
|
-
currentBlock.thinking += part.text;
|
|
725
|
-
currentBlock.thinkingSignature = retainThoughtSignature(
|
|
726
|
-
currentBlock.thinkingSignature,
|
|
784
|
+
if (isThinking) {
|
|
785
|
+
flushVisibleText();
|
|
786
|
+
const block = startThinkingBlock();
|
|
787
|
+
block.thinking += part.text;
|
|
788
|
+
block.thinkingSignature = retainThoughtSignature(
|
|
789
|
+
block.thinkingSignature,
|
|
727
790
|
part.thoughtSignature,
|
|
728
791
|
);
|
|
729
792
|
stream.push({
|
|
@@ -744,7 +807,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
744
807
|
textBuffer = part.text;
|
|
745
808
|
bufferedTextSignature = part.thoughtSignature;
|
|
746
809
|
} else {
|
|
747
|
-
|
|
810
|
+
feedVisibleText(part.text, part.thoughtSignature);
|
|
748
811
|
}
|
|
749
812
|
|
|
750
813
|
if (isBuffering) {
|
|
@@ -757,32 +820,19 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
757
820
|
isBuffering = false;
|
|
758
821
|
textBuffer = "";
|
|
759
822
|
bufferedTextSignature = undefined;
|
|
760
|
-
|
|
823
|
+
feedVisibleText(buffered.visibleText, visibleSignature);
|
|
761
824
|
}
|
|
762
825
|
}
|
|
763
826
|
}
|
|
764
|
-
} else if (part.text === "" && part.thoughtSignature &&
|
|
765
|
-
|
|
766
|
-
currentBlock.thinkingSignature = retainThoughtSignature(
|
|
767
|
-
currentBlock.thinkingSignature,
|
|
768
|
-
part.thoughtSignature,
|
|
769
|
-
);
|
|
770
|
-
} else {
|
|
771
|
-
currentBlock.textSignature = retainThoughtSignature(
|
|
772
|
-
currentBlock.textSignature,
|
|
773
|
-
part.thoughtSignature,
|
|
774
|
-
);
|
|
775
|
-
}
|
|
827
|
+
} else if (part.text === "" && part.thoughtSignature && !part.functionCall) {
|
|
828
|
+
retainCurrentBlockThoughtSignature(part.thoughtSignature);
|
|
776
829
|
}
|
|
777
830
|
|
|
778
831
|
if (part.functionCall) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
currentBlock = null;
|
|
782
|
-
}
|
|
832
|
+
flushVisibleText();
|
|
833
|
+
endCurrentBlock();
|
|
783
834
|
isBuffering = false;
|
|
784
835
|
textBuffer = "";
|
|
785
|
-
|
|
786
836
|
const providedId = part.functionCall.id;
|
|
787
837
|
const needsNewId =
|
|
788
838
|
!providedId || output.content.some(b => b.type === "toolCall" && b.id === providedId);
|
|
@@ -848,16 +898,15 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
848
898
|
sawLeak = true;
|
|
849
899
|
}
|
|
850
900
|
if (buffered.kind !== "incomplete") {
|
|
851
|
-
|
|
901
|
+
feedVisibleText(buffered.visibleText, bufferedTextSignature);
|
|
852
902
|
}
|
|
853
903
|
bufferedTextSignature = undefined;
|
|
854
904
|
isBuffering = false;
|
|
855
905
|
textBuffer = "";
|
|
856
906
|
}
|
|
857
907
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
908
|
+
flushVisibleText(bufferedTextSignature);
|
|
909
|
+
endCurrentBlock();
|
|
861
910
|
|
|
862
911
|
return hasMeaningfulGoogleContent(output) || sawLeak;
|
|
863
912
|
};
|