@oh-my-pi/pi-ai 16.2.6 → 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 +22 -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 +1 -1
- package/src/providers/openai-shared.ts +5 -11
- package/src/stream.ts +21 -6
- package/src/types.ts +164 -51
- package/src/utils/leaked-thinking-stream.ts +260 -0
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
FetchImpl,
|
|
15
15
|
ImageContent,
|
|
16
16
|
Model,
|
|
17
|
+
ServiceTier,
|
|
17
18
|
StopReason,
|
|
18
19
|
StreamOptions,
|
|
19
20
|
TextContent,
|
|
@@ -21,6 +22,7 @@ import type {
|
|
|
21
22
|
Tool,
|
|
22
23
|
ToolCall,
|
|
23
24
|
} from "../types";
|
|
25
|
+
import { shouldSendServiceTier } from "../types";
|
|
24
26
|
import { normalizeSystemPrompts } from "../utils";
|
|
25
27
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
26
28
|
import type { RawHttpRequestDump } from "../utils/http-inspector";
|
|
@@ -73,6 +75,21 @@ export interface GoogleSharedStreamOptions extends StreamOptions {
|
|
|
73
75
|
budgetTokens?: number;
|
|
74
76
|
level?: GoogleThinkingLevel;
|
|
75
77
|
};
|
|
78
|
+
/** Gemini/Vertex serving tier (`flex`/`priority`); other values are omitted. */
|
|
79
|
+
serviceTier?: ServiceTier;
|
|
80
|
+
/**
|
|
81
|
+
* Continues a Gemini Interactions API conversation from a stored interaction.
|
|
82
|
+
* When set on the direct Google provider, the request uses `/interactions`
|
|
83
|
+
* with `previous_interaction_id` instead of the legacy generateContent stream.
|
|
84
|
+
*/
|
|
85
|
+
previousInteractionId?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Uses the Gemini Interactions API for direct Google requests, storing the
|
|
88
|
+
* returned interaction id on the assistant response for follow-up turns.
|
|
89
|
+
*/
|
|
90
|
+
useInteractionsApi?: boolean;
|
|
91
|
+
/** Overrides Interactions API request storage; default is the API default (`true`). */
|
|
92
|
+
storeInteraction?: boolean;
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
/**
|
|
@@ -126,11 +143,9 @@ function resolveThoughtSignature(isSameProviderAndModel: boolean, signature: str
|
|
|
126
143
|
return isSameProviderAndModel && isValidThoughtSignature(signature) ? signature : undefined;
|
|
127
144
|
}
|
|
128
145
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
export function requiresToolCallId(modelId: string): boolean {
|
|
133
|
-
return modelId.startsWith("claude-");
|
|
146
|
+
function supportsFunctionPartId<T extends GoogleApiType>(model: Model<T>): boolean {
|
|
147
|
+
if (model.api === "google-vertex") return false;
|
|
148
|
+
return model.id.startsWith("claude-") || (model.api === "google-generative-ai" && isGemini3Model(model.id));
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
function getGeminiMajorVersion(modelId: string): number | undefined {
|
|
@@ -156,8 +171,9 @@ function isGemini3Model(modelId: string): boolean {
|
|
|
156
171
|
*/
|
|
157
172
|
export function convertMessages<T extends GoogleApiType>(model: Model<T>, context: Context): Content[] {
|
|
158
173
|
const contents: Content[] = [];
|
|
174
|
+
const emittedToolCallNames = new Map<string, string>();
|
|
175
|
+
|
|
159
176
|
const normalizeToolCallId = (id: string): string => {
|
|
160
|
-
if (!requiresToolCallId(model.id)) return id;
|
|
161
177
|
return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
162
178
|
};
|
|
163
179
|
|
|
@@ -243,6 +259,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
243
259
|
});
|
|
244
260
|
}
|
|
245
261
|
} else if (block.type === "toolCall") {
|
|
262
|
+
emittedToolCallNames.set(block.id, block.name);
|
|
246
263
|
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature);
|
|
247
264
|
const effectiveSignature =
|
|
248
265
|
thoughtSignature || (isGemini3Model(model.id) ? SKIP_THOUGHT_SIGNATURE : undefined);
|
|
@@ -251,11 +268,11 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
251
268
|
functionCall: {
|
|
252
269
|
name: block.name,
|
|
253
270
|
args: block.arguments ?? {},
|
|
254
|
-
...(
|
|
271
|
+
...(supportsFunctionPartId(model) ? { id: block.id } : {}),
|
|
255
272
|
},
|
|
256
273
|
};
|
|
257
274
|
if (model.provider === "google-vertex" && part?.functionCall?.id) {
|
|
258
|
-
delete part.functionCall.id; // Vertex AI
|
|
275
|
+
delete part.functionCall.id; // Vertex AI GenerateContent rejects 'id' in functionCall parts.
|
|
259
276
|
}
|
|
260
277
|
if (effectiveSignature) {
|
|
261
278
|
part.thoughtSignature = effectiveSignature;
|
|
@@ -301,10 +318,11 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
301
318
|
},
|
|
302
319
|
}));
|
|
303
320
|
|
|
304
|
-
const includeId =
|
|
321
|
+
const includeId = supportsFunctionPartId(model);
|
|
322
|
+
const emittedName = emittedToolCallNames.get(msg.toolCallId);
|
|
305
323
|
const functionResponsePart: Part = {
|
|
306
324
|
functionResponse: {
|
|
307
|
-
name: msg.toolName,
|
|
325
|
+
name: emittedName ?? msg.toolName,
|
|
308
326
|
response: msg.isError ? { error: responseValue } : { output: responseValue },
|
|
309
327
|
...(hasImages && modelSupportsMultimodalFunctionResponse && { parts: imageParts }),
|
|
310
328
|
...(includeId ? { id: msg.toolCallId } : {}),
|
|
@@ -312,7 +330,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
312
330
|
};
|
|
313
331
|
|
|
314
332
|
if (model.provider === "google-vertex" && functionResponsePart.functionResponse?.id) {
|
|
315
|
-
delete functionResponsePart.functionResponse.id; // Vertex AI
|
|
333
|
+
delete functionResponsePart.functionResponse.id; // Vertex AI GenerateContent rejects 'id' in functionResponse parts.
|
|
316
334
|
}
|
|
317
335
|
|
|
318
336
|
// Cloud Code Assist API requires all function responses to be in a single user turn.
|
|
@@ -529,6 +547,24 @@ export function pushToolCallEvents(
|
|
|
529
547
|
* `text_start` / `thinking_start` event. `onBeforeStartEvent` lets the SSE consumer
|
|
530
548
|
* inject its `ensureStarted()` first-token side effect into the canonical event order.
|
|
531
549
|
*/
|
|
550
|
+
export function startTextOrThinkingBlock(
|
|
551
|
+
isThinking: true,
|
|
552
|
+
output: AssistantMessage,
|
|
553
|
+
stream: AssistantMessageEventStream,
|
|
554
|
+
onBeforeStartEvent?: () => void,
|
|
555
|
+
): ThinkingContent;
|
|
556
|
+
export function startTextOrThinkingBlock(
|
|
557
|
+
isThinking: false,
|
|
558
|
+
output: AssistantMessage,
|
|
559
|
+
stream: AssistantMessageEventStream,
|
|
560
|
+
onBeforeStartEvent?: () => void,
|
|
561
|
+
): TextContent;
|
|
562
|
+
export function startTextOrThinkingBlock(
|
|
563
|
+
isThinking: boolean,
|
|
564
|
+
output: AssistantMessage,
|
|
565
|
+
stream: AssistantMessageEventStream,
|
|
566
|
+
onBeforeStartEvent?: () => void,
|
|
567
|
+
): TextContent | ThinkingContent;
|
|
532
568
|
export function startTextOrThinkingBlock(
|
|
533
569
|
isThinking: boolean,
|
|
534
570
|
output: AssistantMessage,
|
|
@@ -791,6 +827,14 @@ export function buildGoogleGenerateContentParams<T extends "google-generative-ai
|
|
|
791
827
|
...(context.tools && context.tools.length > 0 && { tools: convertTools(context.tools, model) }),
|
|
792
828
|
};
|
|
793
829
|
|
|
830
|
+
// Gemini API (google-generative-ai) reads the tier from the request body;
|
|
831
|
+
// Vertex AI ignores a body field and requires the
|
|
832
|
+
// `X-Vertex-AI-LLM-Shared-Request-Type` header instead (added in
|
|
833
|
+
// streamGoogleVertex), so only emit the body field for the direct API.
|
|
834
|
+
if (model.provider === "google" && shouldSendServiceTier(options.serviceTier, model.provider)) {
|
|
835
|
+
config.serviceTier = options.serviceTier;
|
|
836
|
+
}
|
|
837
|
+
|
|
794
838
|
if (context.tools && context.tools.length > 0 && options.toolChoice) {
|
|
795
839
|
const choice = options.toolChoice;
|
|
796
840
|
if (typeof choice === "string") {
|
|
@@ -852,6 +896,8 @@ export interface GoogleGenAIRequestPlan {
|
|
|
852
896
|
url: string;
|
|
853
897
|
headers: Record<string, string>;
|
|
854
898
|
fetch?: FetchImpl;
|
|
899
|
+
/** Optional URL retried once when {@link url} returns 404 (regional Vertex endpoint missing a global-only model). */
|
|
900
|
+
fallbackUrl?: string;
|
|
855
901
|
}
|
|
856
902
|
|
|
857
903
|
export function streamGoogleGenAI<T extends "google-generative-ai" | "google-vertex">(args: {
|
|
@@ -906,8 +952,8 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
|
|
906
952
|
|
|
907
953
|
const bodyJson = JSON.stringify(paramsToWireBody(params));
|
|
908
954
|
const fetchImpl = plan.fetch ?? options?.fetch ?? (globalThis.fetch.bind(globalThis) as FetchImpl);
|
|
909
|
-
const
|
|
910
|
-
const response = await fetchImpl(
|
|
955
|
+
const openStreamAt = async (requestUrl: string): Promise<ReadableStream<Uint8Array>> => {
|
|
956
|
+
const response = await fetchImpl(requestUrl, {
|
|
911
957
|
method: "POST",
|
|
912
958
|
headers: { ...plan.headers, "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
913
959
|
body: bodyJson,
|
|
@@ -929,6 +975,20 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
|
|
929
975
|
}
|
|
930
976
|
return response.body as ReadableStream<Uint8Array>;
|
|
931
977
|
};
|
|
978
|
+
// A regional Vertex endpoint 404s for models published only on the
|
|
979
|
+
// global endpoint; retry global once so a stale/ambient region never
|
|
980
|
+
// breaks a request that worked before regional routing existed.
|
|
981
|
+
const openStream = async (): Promise<ReadableStream<Uint8Array>> => {
|
|
982
|
+
if (!plan.fallbackUrl) return openStreamAt(plan.url);
|
|
983
|
+
try {
|
|
984
|
+
return await openStreamAt(plan.url);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
if (error instanceof AIError.GoogleApiError && error.status === 404) {
|
|
987
|
+
return openStreamAt(plan.fallbackUrl);
|
|
988
|
+
}
|
|
989
|
+
throw error;
|
|
990
|
+
}
|
|
991
|
+
};
|
|
932
992
|
|
|
933
993
|
let body = await openStream();
|
|
934
994
|
stream.push({ type: "start", partial: output });
|
|
@@ -1007,6 +1067,7 @@ function paramsToWireBody(params: GenerateContentParameters): Record<string, unk
|
|
|
1007
1067
|
if (config.toolConfig !== undefined) body.toolConfig = config.toolConfig;
|
|
1008
1068
|
if (config.safetySettings !== undefined) body.safetySettings = config.safetySettings;
|
|
1009
1069
|
if (config.cachedContent !== undefined) body.cachedContent = config.cachedContent;
|
|
1070
|
+
if (config.serviceTier !== undefined) body.serviceTier = config.serviceTier;
|
|
1010
1071
|
|
|
1011
1072
|
const gen: Record<string, unknown> = {};
|
|
1012
1073
|
if (config.temperature !== undefined) gen.temperature = config.temperature;
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* - `POST {generativelanguage,aiplatform}.googleapis.com/.../models/{model}:streamGenerateContent?alt=sse`
|
|
10
10
|
* - The Cloud Code Assist endpoint used by `google-gemini-cli.ts`
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
12
|
/** Mirror of `@google/genai`'s `FinishReason` string enum. */
|
|
14
13
|
export type FinishReason =
|
|
15
14
|
| "FINISH_REASON_UNSPECIFIED"
|
|
@@ -131,6 +130,11 @@ export interface GenerateContentConfig {
|
|
|
131
130
|
safetySettings?: Array<Record<string, unknown>>;
|
|
132
131
|
cachedContent?: string;
|
|
133
132
|
thinkingConfig?: ThinkingConfig;
|
|
133
|
+
/**
|
|
134
|
+
* Gemini/Vertex serving tier. Serialized to the request body root as
|
|
135
|
+
* `serviceTier` (camelCase) by the transformer in `google-shared.ts`.
|
|
136
|
+
*/
|
|
137
|
+
serviceTier?: "auto" | "default" | "flex" | "scale" | "priority";
|
|
134
138
|
abortSignal?: AbortSignal;
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -2,7 +2,13 @@ import { $env } from "@oh-my-pi/pi-utils";
|
|
|
2
2
|
import * as AIError from "../error";
|
|
3
3
|
import type { Context, Model, StreamFunction } from "../types";
|
|
4
4
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
5
|
-
import { getVertexAccessToken } from "./google-auth";
|
|
5
|
+
import { getVertexAccessToken, hasVertexBearerCredentialsHint } from "./google-auth";
|
|
6
|
+
import {
|
|
7
|
+
type GoogleInteractionsPlan,
|
|
8
|
+
modelSupportsInteractions,
|
|
9
|
+
resolveInteractionDispatch,
|
|
10
|
+
streamGoogleInteractions,
|
|
11
|
+
} from "./google-interactions";
|
|
6
12
|
import {
|
|
7
13
|
buildGoogleGenerateContentParams,
|
|
8
14
|
type GoogleGenAIRequestPlan,
|
|
@@ -16,48 +22,128 @@ export interface GoogleVertexOptions extends GoogleSharedStreamOptions {
|
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
const API_VERSION = "v1";
|
|
25
|
+
const INTERACTIONS_API_VERSION = "v1beta1";
|
|
26
|
+
const INTERACTIONS_API_REVISION = "2026-05-20";
|
|
19
27
|
|
|
20
28
|
export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
|
|
21
29
|
model: Model<"google-vertex">,
|
|
22
30
|
context: Context,
|
|
23
31
|
options?: GoogleVertexOptions,
|
|
24
|
-
): AssistantMessageEventStream =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
): AssistantMessageEventStream => {
|
|
33
|
+
const runGenerateContent = (): AssistantMessageEventStream =>
|
|
34
|
+
streamGoogleGenAI({
|
|
35
|
+
model,
|
|
36
|
+
options,
|
|
37
|
+
api: "google-vertex",
|
|
38
|
+
retainTextSignature: true,
|
|
39
|
+
prepare: async (): Promise<GoogleGenAIRequestPlan> => {
|
|
40
|
+
const apiKey = resolveApiKey(options);
|
|
41
|
+
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
|
|
42
|
+
params.config ||= {};
|
|
43
|
+
if (!params.config.safetySettings) {
|
|
44
|
+
params.config.safetySettings = [
|
|
45
|
+
{
|
|
46
|
+
category: "HARM_CATEGORY_HATE_SPEECH",
|
|
47
|
+
threshold: "OFF",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
51
|
+
threshold: "OFF",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
55
|
+
threshold: "OFF",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
category: "HARM_CATEGORY_HARASSMENT",
|
|
59
|
+
threshold: "OFF",
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
const baseHeaders: Record<string, string> = {
|
|
64
|
+
...(model.headers ?? {}),
|
|
65
|
+
...(options?.headers ?? {}),
|
|
66
|
+
};
|
|
67
|
+
// Vertex AI ignores a `serviceTier` request-body field (unlike the direct
|
|
68
|
+
// Gemini API); priority must travel as a request header. Only `priority`
|
|
69
|
+
// has a documented Vertex request control — `flex` has none, so it's a no-op.
|
|
70
|
+
if (options?.serviceTier === "priority") {
|
|
71
|
+
baseHeaders["X-Vertex-AI-LLM-Shared-Request-Type"] = "priority";
|
|
72
|
+
}
|
|
37
73
|
|
|
38
|
-
|
|
39
|
-
|
|
74
|
+
if (apiKey) {
|
|
75
|
+
// Explicit `location` is a deliberate residency choice: honor it and let
|
|
76
|
+
// a 404 surface. An ambient env-derived region falls back to the global
|
|
77
|
+
// endpoint so a stray GOOGLE_*_LOCATION never breaks a previously-working
|
|
78
|
+
// global-only request.
|
|
79
|
+
const explicitLocation = options?.location;
|
|
80
|
+
const location = explicitLocation ?? resolveAmbientLocation() ?? "global";
|
|
81
|
+
const host = resolveEndpointHost(location);
|
|
82
|
+
const path = `${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
|
83
|
+
const useGlobalFallback = !explicitLocation && host !== "aiplatform.googleapis.com";
|
|
84
|
+
return {
|
|
85
|
+
params,
|
|
86
|
+
url: `https://${host}/${path}`,
|
|
87
|
+
fallbackUrl: useGlobalFallback ? `https://aiplatform.googleapis.com/${path}` : undefined,
|
|
88
|
+
headers: {
|
|
89
|
+
...baseHeaders,
|
|
90
|
+
"x-goog-api-key": apiKey,
|
|
91
|
+
},
|
|
92
|
+
fetch: options?.fetch,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const project = resolveProject(options);
|
|
97
|
+
const location = resolveLocation(options);
|
|
98
|
+
const accessToken = await getVertexAccessToken({ signal: options?.signal, fetch: options?.fetch });
|
|
99
|
+
const host = resolveEndpointHost(location);
|
|
100
|
+
const url = `https://${host}/${API_VERSION}/projects/${project}/locations/${location}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
|
40
101
|
return {
|
|
41
102
|
params,
|
|
42
103
|
url,
|
|
43
|
-
headers: { ...baseHeaders,
|
|
104
|
+
headers: { ...baseHeaders, Authorization: `Bearer ${accessToken}` },
|
|
44
105
|
fetch: options?.fetch,
|
|
45
106
|
};
|
|
46
|
-
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
47
109
|
|
|
110
|
+
// Default Gemini 3+ onto Interactions whenever a bearer credential source exists (ADC file,
|
|
111
|
+
// `GOOGLE_APPLICATION_CREDENTIALS`, or an explicit access-token env). Interactions needs bearer
|
|
112
|
+
// auth, so express API-key-only setups stay on generateContent — and an express key, when
|
|
113
|
+
// present, still serves the generateContent fallback. Interactions always targets the official
|
|
114
|
+
// global `aiplatform` host; the fallback also recovers ids the endpoint rejects.
|
|
115
|
+
const { useInteractions, auto, anchor, state } = resolveInteractionDispatch({
|
|
116
|
+
context,
|
|
117
|
+
options,
|
|
118
|
+
provider: model.provider,
|
|
119
|
+
autoEligible: modelSupportsInteractions(model) && hasVertexBearerCredentialsHint(),
|
|
120
|
+
});
|
|
121
|
+
if (!useInteractions) return runGenerateContent();
|
|
122
|
+
|
|
123
|
+
return streamGoogleInteractions({
|
|
124
|
+
model,
|
|
125
|
+
context,
|
|
126
|
+
options,
|
|
127
|
+
api: "google-vertex",
|
|
128
|
+
anchor,
|
|
129
|
+
state,
|
|
130
|
+
prepare: async (): Promise<GoogleInteractionsPlan> => {
|
|
48
131
|
const project = resolveProject(options);
|
|
49
|
-
const location = resolveLocation(options);
|
|
50
132
|
const accessToken = await getVertexAccessToken({ signal: options?.signal, fetch: options?.fetch });
|
|
51
|
-
const host = resolveEndpointHost(location);
|
|
52
|
-
const url = `https://${host}/${API_VERSION}/projects/${project}/locations/${location}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
|
53
133
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
134
|
+
url: `https://aiplatform.googleapis.com/${INTERACTIONS_API_VERSION}/projects/${project}/locations/global/interactions`,
|
|
135
|
+
headers: {
|
|
136
|
+
...(model.headers ?? {}),
|
|
137
|
+
...(options?.headers ?? {}),
|
|
138
|
+
Authorization: `Bearer ${accessToken}`,
|
|
139
|
+
"Api-Revision": INTERACTIONS_API_REVISION,
|
|
140
|
+
},
|
|
57
141
|
fetch: options?.fetch,
|
|
58
142
|
};
|
|
59
143
|
},
|
|
144
|
+
fallback: auto ? runGenerateContent : undefined,
|
|
60
145
|
});
|
|
146
|
+
};
|
|
61
147
|
|
|
62
148
|
function resolveApiKey(options?: GoogleVertexOptions): string | undefined {
|
|
63
149
|
// options.apiKey may contain sentinel values like "<authenticated>" or "N/A"
|
|
@@ -80,9 +166,14 @@ function resolveProject(options?: GoogleVertexOptions): string {
|
|
|
80
166
|
function resolveEndpointHost(location: string): string {
|
|
81
167
|
return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
|
|
82
168
|
}
|
|
169
|
+
function resolveAmbientLocation(): string | undefined {
|
|
170
|
+
return $env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION || undefined;
|
|
171
|
+
}
|
|
172
|
+
function resolveOptionalLocation(options?: GoogleVertexOptions): string | undefined {
|
|
173
|
+
return options?.location || resolveAmbientLocation();
|
|
174
|
+
}
|
|
83
175
|
function resolveLocation(options?: GoogleVertexOptions): string {
|
|
84
|
-
const location =
|
|
85
|
-
options?.location || $env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION;
|
|
176
|
+
const location = resolveOptionalLocation(options);
|
|
86
177
|
if (!location) {
|
|
87
178
|
throw new AIError.ConfigurationError(
|
|
88
179
|
"Vertex AI requires a location. Set GOOGLE_VERTEX_LOCATION/GOOGLE_CLOUD_LOCATION/VERTEX_LOCATION or pass location in options.",
|
package/src/providers/google.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as AIError from "../error";
|
|
|
2
2
|
import { getEnvApiKey } from "../stream";
|
|
3
3
|
import type { Context, Model, StreamFunction } from "../types";
|
|
4
4
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
5
|
+
import { modelSupportsInteractions, resolveInteractionDispatch, streamGoogleInteractions } from "./google-interactions";
|
|
5
6
|
import {
|
|
6
7
|
buildGoogleGenerateContentParams,
|
|
7
8
|
type GoogleGenAIRequestPlan,
|
|
@@ -17,29 +18,70 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
|
|
|
17
18
|
model: Model<"google-generative-ai">,
|
|
18
19
|
context: Context,
|
|
19
20
|
options?: GoogleOptions,
|
|
20
|
-
): AssistantMessageEventStream =>
|
|
21
|
-
|
|
21
|
+
): AssistantMessageEventStream => {
|
|
22
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider);
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
throw new AIError.MissingApiKeyError(
|
|
25
|
+
undefined,
|
|
26
|
+
"Google Generative AI requires an API key (GEMINI_API_KEY or options.apiKey).",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const runGenerateContent = (): AssistantMessageEventStream =>
|
|
31
|
+
streamGoogleGenAI({
|
|
32
|
+
model,
|
|
33
|
+
options,
|
|
34
|
+
api: "google-generative-ai",
|
|
35
|
+
prepare: (): GoogleGenAIRequestPlan => {
|
|
36
|
+
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
|
|
37
|
+
// `model.baseUrl` already includes the API version segment when set (mirrors the
|
|
38
|
+
// `apiVersion: ""` reset that the SDK relied on for custom base URLs).
|
|
39
|
+
const base = model.baseUrl?.trim() || DEFAULT_GENERATIVE_LANGUAGE_BASE;
|
|
40
|
+
const url = `${base}/models/${model.id}:streamGenerateContent?alt=sse`;
|
|
41
|
+
const headers: Record<string, string> = {
|
|
42
|
+
"x-goog-api-key": apiKey,
|
|
43
|
+
...(model.headers ?? {}),
|
|
44
|
+
...(options?.headers ?? {}),
|
|
45
|
+
};
|
|
46
|
+
return { params, url, headers, fetch: options?.fetch };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Default Gemini 3+ on the official endpoint onto Interactions (custom proxy base URLs keep
|
|
51
|
+
// generateContent, which serves the full catalog). The fallback recovers ids the endpoint rejects.
|
|
52
|
+
const trimmedBase = model.baseUrl?.trim();
|
|
53
|
+
let officialEndpoint = !trimmedBase;
|
|
54
|
+
if (trimmedBase) {
|
|
55
|
+
try {
|
|
56
|
+
officialEndpoint = new URL(trimmedBase).hostname === "generativelanguage.googleapis.com";
|
|
57
|
+
} catch {
|
|
58
|
+
officialEndpoint = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const { useInteractions, auto, anchor, state } = resolveInteractionDispatch({
|
|
62
|
+
context,
|
|
63
|
+
options,
|
|
64
|
+
provider: model.provider,
|
|
65
|
+
autoEligible: officialEndpoint && modelSupportsInteractions(model),
|
|
66
|
+
});
|
|
67
|
+
if (!useInteractions) return runGenerateContent();
|
|
68
|
+
|
|
69
|
+
return streamGoogleInteractions({
|
|
22
70
|
model,
|
|
71
|
+
context,
|
|
23
72
|
options,
|
|
24
73
|
api: "google-generative-ai",
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"Google Generative AI requires an API key (GEMINI_API_KEY or options.apiKey).",
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
|
|
34
|
-
// `model.baseUrl` already includes the API version segment when set (mirrors the
|
|
35
|
-
// `apiVersion: ""` reset that the SDK relied on for custom base URLs).
|
|
36
|
-
const base = model.baseUrl?.trim() || DEFAULT_GENERATIVE_LANGUAGE_BASE;
|
|
37
|
-
const url = `${base}/models/${model.id}:streamGenerateContent?alt=sse`;
|
|
38
|
-
const headers: Record<string, string> = {
|
|
74
|
+
anchor,
|
|
75
|
+
state,
|
|
76
|
+
prepare: () => ({
|
|
77
|
+
url: `${trimmedBase || DEFAULT_GENERATIVE_LANGUAGE_BASE}/interactions`,
|
|
78
|
+
headers: {
|
|
39
79
|
"x-goog-api-key": apiKey,
|
|
40
80
|
...(model.headers ?? {}),
|
|
41
81
|
...(options?.headers ?? {}),
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
},
|
|
82
|
+
},
|
|
83
|
+
fetch: options?.fetch,
|
|
84
|
+
}),
|
|
85
|
+
fallback: auto ? runGenerateContent : undefined,
|
|
45
86
|
});
|
|
87
|
+
};
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
Context,
|
|
14
14
|
ImageContent,
|
|
15
15
|
Message,
|
|
16
|
-
|
|
16
|
+
ServiceTier,
|
|
17
17
|
StopReason,
|
|
18
18
|
TextContent,
|
|
19
19
|
Tool,
|
|
@@ -38,7 +38,7 @@ function isReasoningEffort(value: unknown): value is ReasoningEffort {
|
|
|
38
38
|
return value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh";
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function isServiceTier(value: unknown): value is
|
|
41
|
+
function isServiceTier(value: unknown): value is ServiceTier {
|
|
42
42
|
return value === "auto" || value === "default" || value === "flex" || value === "scale" || value === "priority";
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
2
|
+
import { supportsAllTurnsReasoningContext } from "@oh-my-pi/pi-catalog/identity";
|
|
2
3
|
import { requireSupportedEffort } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
3
4
|
import type { Api, Model } from "../../types";
|
|
4
5
|
|
|
@@ -14,7 +15,7 @@ export interface ReasoningConfig {
|
|
|
14
15
|
export interface CodexRequestOptions {
|
|
15
16
|
reasoningEffort?: ReasoningConfig["effort"];
|
|
16
17
|
reasoningSummary?: ReasoningConfig["summary"] | null;
|
|
17
|
-
/** Explicit `reasoning.context` override; defaults to `all_turns`
|
|
18
|
+
/** Explicit `reasoning.context` override; defaults to `all_turns` when unset. The `all_turns` value is gated to gpt-5.4+ Codex models — older ids reject it, so it is suppressed and `context` omitted. */
|
|
18
19
|
reasoningContext?: CodexReasoningContext;
|
|
19
20
|
textVerbosity?: "low" | "medium" | "high";
|
|
20
21
|
include?: string[];
|
|
@@ -254,9 +255,21 @@ export async function transformRequestBody(
|
|
|
254
255
|
...body.reasoning,
|
|
255
256
|
...reasoningConfig,
|
|
256
257
|
};
|
|
257
|
-
// Default reasoning replay to `all_turns
|
|
258
|
-
//
|
|
259
|
-
|
|
258
|
+
// Default reasoning replay to `all_turns`, mirroring codex-rs; an
|
|
259
|
+
// explicit `reasoningContext` overrides the default. The `all_turns`
|
|
260
|
+
// value is only accepted from gpt-5.4 onward — earlier Codex ids
|
|
261
|
+
// (gpt-5.1-codex, gpt-5.3-codex, gpt-5.3-codex-spark) reject it with
|
|
262
|
+
// "Unsupported value: 'all_turns' is not supported with this model".
|
|
263
|
+
// For those, drop `context` so the server applies its `current_turn`
|
|
264
|
+
// default. The version gate is authoritative: even an explicit
|
|
265
|
+
// `all_turns` override is suppressed on unsupported models, while
|
|
266
|
+
// `current_turn`/`auto` (universally supported) always pass through.
|
|
267
|
+
const context = options.reasoningContext ?? "all_turns";
|
|
268
|
+
if (context === "all_turns" && !supportsAllTurnsReasoningContext(model.id)) {
|
|
269
|
+
delete body.reasoning.context;
|
|
270
|
+
} else {
|
|
271
|
+
body.reasoning.context = context;
|
|
272
|
+
}
|
|
260
273
|
} else {
|
|
261
274
|
delete body.reasoning;
|
|
262
275
|
}
|
|
@@ -104,7 +104,7 @@ import { transformMessages } from "./transform-messages";
|
|
|
104
104
|
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
105
105
|
reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
106
106
|
reasoningSummary?: "auto" | "concise" | "detailed" | null;
|
|
107
|
-
/** `reasoning.context` replay scope; defaults to `all_turns`
|
|
107
|
+
/** `reasoning.context` replay scope; defaults to `all_turns` when unset. The `all_turns` value is gated to gpt-5.4+ Codex models — older ids reject it, so it is suppressed and `context` omitted. */
|
|
108
108
|
reasoningContext?: CodexReasoningContext;
|
|
109
109
|
textVerbosity?: "low" | "medium" | "high";
|
|
110
110
|
include?: string[];
|
|
@@ -39,8 +39,6 @@ import {
|
|
|
39
39
|
type Model,
|
|
40
40
|
OPENAI_MAX_OUTPUT_TOKENS,
|
|
41
41
|
type Provider,
|
|
42
|
-
type ResolvedServiceTier,
|
|
43
|
-
resolveServiceTier,
|
|
44
42
|
type ServiceTier,
|
|
45
43
|
type StopReason,
|
|
46
44
|
type StreamOptions,
|
|
@@ -269,14 +267,13 @@ export function resolveOpenAIRequestSetup(
|
|
|
269
267
|
}
|
|
270
268
|
|
|
271
269
|
export function applyOpenAIServiceTier(
|
|
272
|
-
params: { service_tier?:
|
|
270
|
+
params: { service_tier?: ServiceTier | null | undefined },
|
|
273
271
|
serviceTier: ServiceTier | null | undefined,
|
|
274
272
|
provider: Provider | undefined,
|
|
275
273
|
): void {
|
|
276
274
|
if (!shouldSendServiceTier(serviceTier, provider)) return;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
params.service_tier = resolved;
|
|
275
|
+
if (serviceTier === "flex" || serviceTier === "scale" || serviceTier === "priority") {
|
|
276
|
+
params.service_tier = serviceTier;
|
|
280
277
|
}
|
|
281
278
|
}
|
|
282
279
|
|
|
@@ -315,10 +312,7 @@ export function applyOpenAIResponsesServiceTierCost(
|
|
|
315
312
|
// The response echo is authoritative when present (OpenAI may downgrade a
|
|
316
313
|
// requested priority/flex turn to default under load); only fall back to the
|
|
317
314
|
// requested tier when the response omits the echo entirely.
|
|
318
|
-
const served =
|
|
319
|
-
typeof responseServiceTier === "string"
|
|
320
|
-
? responseServiceTier
|
|
321
|
-
: resolveServiceTier(requestServiceTier, model.provider);
|
|
315
|
+
const served = typeof responseServiceTier === "string" ? responseServiceTier : (requestServiceTier ?? undefined);
|
|
322
316
|
const multiplier = getOpenAIResponsesServiceTierCostMultiplier(served);
|
|
323
317
|
if (multiplier === 1) return;
|
|
324
318
|
usage.cost.input *= multiplier;
|
|
@@ -623,7 +617,7 @@ export type OpenAICompletionsParams = Omit<ChatCompletionCreateParamsStreaming,
|
|
|
623
617
|
chat_template_kwargs?: { enable_thinking?: boolean; preserve_thinking?: boolean };
|
|
624
618
|
reasoning?: { effort?: string } | { enabled: false };
|
|
625
619
|
reasoning_effort?: string | null;
|
|
626
|
-
service_tier?:
|
|
620
|
+
service_tier?: ServiceTier;
|
|
627
621
|
tool_stream?: boolean;
|
|
628
622
|
provider?: OpenAICompat["openRouterRouting"];
|
|
629
623
|
providerOptions?: { gateway?: { only?: string[]; order?: string[] } };
|