feat(ai/providers): added global fallback for regional vertex endpoints
- Introduce `fallbackUrl` to allow retrying requests on the global Vertex endpoint when a 404 is encountered on a regional endpoint. - Correctly track tool call names to facilitate accurate function response mapping. - Update `serviceTier` type definitions to accurately reflect allowed values. - Refine vertex endpoint location resolution to ensure better compatibility with ambient regional settings.
This commit is contained in:
@@ -15,11 +15,11 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved Vertex AI reliability by automatically falling back to global endpoints on 404 errors
|
||||
|
||||
- Fixed safety setting application for Google Vertex AI models
|
||||
- Ensured Gemini service tier is correctly passed through to the API
|
||||
- Corrected priority request accounting for supported providers
|
||||
### Fixed
|
||||
|
||||
- Fixed Kimi Code's Anthropic-compatible request path to keep thinking enabled and downgrade forced tool choice for Kimi K2.7 Code title generation. ([#3852](https://github.com/can1357/oh-my-pi/issues/3852))
|
||||
|
||||
## [16.2.6] - 2026-06-29
|
||||
|
||||
@@ -130,11 +130,9 @@ function resolveThoughtSignature(isSameProviderAndModel: boolean, signature: str
|
||||
return isSameProviderAndModel && isValidThoughtSignature(signature) ? signature : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude models via Google APIs require explicit tool call IDs in function calls/responses.
|
||||
*/
|
||||
export function requiresToolCallId(modelId: string): boolean {
|
||||
return modelId.startsWith("claude-");
|
||||
function supportsFunctionPartId<T extends GoogleApiType>(model: Model<T>): boolean {
|
||||
if (model.api === "google-vertex") return false;
|
||||
return model.id.startsWith("claude-") || (model.api === "google-generative-ai" && isGemini3Model(model.id));
|
||||
}
|
||||
|
||||
function getGeminiMajorVersion(modelId: string): number | undefined {
|
||||
@@ -160,8 +158,9 @@ function isGemini3Model(modelId: string): boolean {
|
||||
*/
|
||||
export function convertMessages<T extends GoogleApiType>(model: Model<T>, context: Context): Content[] {
|
||||
const contents: Content[] = [];
|
||||
const emittedToolCallNames = new Map<string, string>();
|
||||
|
||||
const normalizeToolCallId = (id: string): string => {
|
||||
if (!requiresToolCallId(model.id)) return id;
|
||||
return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
||||
};
|
||||
|
||||
@@ -247,6 +246,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
||||
});
|
||||
}
|
||||
} else if (block.type === "toolCall") {
|
||||
emittedToolCallNames.set(block.id, block.name);
|
||||
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature);
|
||||
const effectiveSignature =
|
||||
thoughtSignature || (isGemini3Model(model.id) ? SKIP_THOUGHT_SIGNATURE : undefined);
|
||||
@@ -255,11 +255,11 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
||||
functionCall: {
|
||||
name: block.name,
|
||||
args: block.arguments ?? {},
|
||||
...(requiresToolCallId(model.id) ? { id: block.id } : {}),
|
||||
...(supportsFunctionPartId(model) ? { id: block.id } : {}),
|
||||
},
|
||||
};
|
||||
if (model.provider === "google-vertex" && part?.functionCall?.id) {
|
||||
delete part.functionCall.id; // Vertex AI does not support 'id' in functionCall
|
||||
delete part.functionCall.id; // Vertex AI GenerateContent rejects 'id' in functionCall parts.
|
||||
}
|
||||
if (effectiveSignature) {
|
||||
part.thoughtSignature = effectiveSignature;
|
||||
@@ -305,10 +305,11 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
||||
},
|
||||
}));
|
||||
|
||||
const includeId = requiresToolCallId(model.id);
|
||||
const includeId = supportsFunctionPartId(model);
|
||||
const emittedName = emittedToolCallNames.get(msg.toolCallId);
|
||||
const functionResponsePart: Part = {
|
||||
functionResponse: {
|
||||
name: msg.toolName,
|
||||
name: emittedName ?? msg.toolName,
|
||||
response: msg.isError ? { error: responseValue } : { output: responseValue },
|
||||
...(hasImages && modelSupportsMultimodalFunctionResponse && { parts: imageParts }),
|
||||
...(includeId ? { id: msg.toolCallId } : {}),
|
||||
@@ -316,7 +317,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
||||
};
|
||||
|
||||
if (model.provider === "google-vertex" && functionResponsePart.functionResponse?.id) {
|
||||
delete functionResponsePart.functionResponse.id; // Vertex AI does not support 'id' in functionResponse
|
||||
delete functionResponsePart.functionResponse.id; // Vertex AI GenerateContent rejects 'id' in functionResponse parts.
|
||||
}
|
||||
|
||||
// Cloud Code Assist API requires all function responses to be in a single user turn.
|
||||
@@ -533,6 +534,24 @@ export function pushToolCallEvents(
|
||||
* `text_start` / `thinking_start` event. `onBeforeStartEvent` lets the SSE consumer
|
||||
* inject its `ensureStarted()` first-token side effect into the canonical event order.
|
||||
*/
|
||||
export function startTextOrThinkingBlock(
|
||||
isThinking: true,
|
||||
output: AssistantMessage,
|
||||
stream: AssistantMessageEventStream,
|
||||
onBeforeStartEvent?: () => void,
|
||||
): ThinkingContent;
|
||||
export function startTextOrThinkingBlock(
|
||||
isThinking: false,
|
||||
output: AssistantMessage,
|
||||
stream: AssistantMessageEventStream,
|
||||
onBeforeStartEvent?: () => void,
|
||||
): TextContent;
|
||||
export function startTextOrThinkingBlock(
|
||||
isThinking: boolean,
|
||||
output: AssistantMessage,
|
||||
stream: AssistantMessageEventStream,
|
||||
onBeforeStartEvent?: () => void,
|
||||
): TextContent | ThinkingContent;
|
||||
export function startTextOrThinkingBlock(
|
||||
isThinking: boolean,
|
||||
output: AssistantMessage,
|
||||
@@ -864,6 +883,8 @@ export interface GoogleGenAIRequestPlan {
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
fetch?: FetchImpl;
|
||||
/** Optional URL retried once when {@link url} returns 404 (regional Vertex endpoint missing a global-only model). */
|
||||
fallbackUrl?: string;
|
||||
}
|
||||
|
||||
export function streamGoogleGenAI<T extends "google-generative-ai" | "google-vertex">(args: {
|
||||
@@ -918,8 +939,8 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
||||
|
||||
const bodyJson = JSON.stringify(paramsToWireBody(params));
|
||||
const fetchImpl = plan.fetch ?? options?.fetch ?? (globalThis.fetch.bind(globalThis) as FetchImpl);
|
||||
const openStream = async (): Promise<ReadableStream<Uint8Array>> => {
|
||||
const response = await fetchImpl(plan.url, {
|
||||
const openStreamAt = async (requestUrl: string): Promise<ReadableStream<Uint8Array>> => {
|
||||
const response = await fetchImpl(requestUrl, {
|
||||
method: "POST",
|
||||
headers: { ...plan.headers, "Content-Type": "application/json", Accept: "text/event-stream" },
|
||||
body: bodyJson,
|
||||
@@ -941,6 +962,20 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
||||
}
|
||||
return response.body as ReadableStream<Uint8Array>;
|
||||
};
|
||||
// A regional Vertex endpoint 404s for models published only on the
|
||||
// global endpoint; retry global once so a stale/ambient region never
|
||||
// breaks a request that worked before regional routing existed.
|
||||
const openStream = async (): Promise<ReadableStream<Uint8Array>> => {
|
||||
if (!plan.fallbackUrl) return openStreamAt(plan.url);
|
||||
try {
|
||||
return await openStreamAt(plan.url);
|
||||
} catch (error) {
|
||||
if (error instanceof AIError.GoogleApiError && error.status === 404) {
|
||||
return openStreamAt(plan.fallbackUrl);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
let body = await openStream();
|
||||
stream.push({ type: "start", partial: output });
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
* - `POST {generativelanguage,aiplatform}.googleapis.com/.../models/{model}:streamGenerateContent?alt=sse`
|
||||
* - The Cloud Code Assist endpoint used by `google-gemini-cli.ts`
|
||||
*/
|
||||
|
||||
import type { ServiceTier } from "../types";
|
||||
|
||||
/** Mirror of `@google/genai`'s `FinishReason` string enum. */
|
||||
export type FinishReason =
|
||||
| "FINISH_REASON_UNSPECIFIED"
|
||||
@@ -137,7 +134,7 @@ export interface GenerateContentConfig {
|
||||
* Gemini/Vertex serving tier. Serialized to the request body root as
|
||||
* `serviceTier` (camelCase) by the transformer in `google-shared.ts`.
|
||||
*/
|
||||
serviceTier?: ServiceTier;
|
||||
serviceTier?: "auto" | "default" | "flex" | "scale" | "priority";
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,10 +63,19 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
|
||||
}
|
||||
|
||||
if (apiKey) {
|
||||
const url = `https://aiplatform.googleapis.com/${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
||||
// Explicit `location` is a deliberate residency choice: honor it and let
|
||||
// a 404 surface. An ambient env-derived region falls back to the global
|
||||
// endpoint so a stray GOOGLE_*_LOCATION never breaks a previously-working
|
||||
// global-only request.
|
||||
const explicitLocation = options?.location;
|
||||
const location = explicitLocation ?? resolveAmbientLocation() ?? "global";
|
||||
const host = resolveEndpointHost(location);
|
||||
const path = `${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
||||
const useGlobalFallback = !explicitLocation && host !== "aiplatform.googleapis.com";
|
||||
return {
|
||||
params,
|
||||
url,
|
||||
url: `https://${host}/${path}`,
|
||||
fallbackUrl: useGlobalFallback ? `https://aiplatform.googleapis.com/${path}` : undefined,
|
||||
headers: {
|
||||
...baseHeaders,
|
||||
"x-goog-api-key": apiKey,
|
||||
@@ -110,9 +119,14 @@ function resolveProject(options?: GoogleVertexOptions): string {
|
||||
function resolveEndpointHost(location: string): string {
|
||||
return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
|
||||
}
|
||||
function resolveAmbientLocation(): string | undefined {
|
||||
return $env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION || undefined;
|
||||
}
|
||||
function resolveOptionalLocation(options?: GoogleVertexOptions): string | undefined {
|
||||
return options?.location || resolveAmbientLocation();
|
||||
}
|
||||
function resolveLocation(options?: GoogleVertexOptions): string {
|
||||
const location =
|
||||
options?.location || $env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION;
|
||||
const location = resolveOptionalLocation(options);
|
||||
if (!location) {
|
||||
throw new AIError.ConfigurationError(
|
||||
"Vertex AI requires a location. Set GOOGLE_VERTEX_LOCATION/GOOGLE_CLOUD_LOCATION/VERTEX_LOCATION or pass location in options.",
|
||||
|
||||
Reference in New Issue
Block a user