@@ -5,9 +5,11 @@
|
||||
### Added
|
||||
|
||||
- Added GPT-5.5 to OpenAI Codex model generation.
|
||||
- Added `findEnvKeys()` so callers can identify configured provider API-key environment variables without exposing credential values while preserving `getEnvApiKey()` as the credential-value API.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `google-vertex` to forward custom `model.baseUrl` values to `@google/genai`, enabling Vertex proxy and gateway endpoints ([#3619](https://github.com/badlogic/pi-mono/issues/3619))
|
||||
- Fixed OpenAI-compatible completion usage parsing to stop double-counting reasoning tokens already included in `completion_tokens` ([#3581](https://github.com/badlogic/pi-mono/issues/3581))
|
||||
- Fixed long cache retention compatibility by adding `compat.supportsLongCacheRetention`, allowing Anthropic Messages and OpenAI-compatible proxies to explicitly disable long-retention fields while enabling long retention by default when requested ([#3543](https://github.com/badlogic/pi-mono/issues/3543))
|
||||
- Fixed `openai-responses` compatibility by adding `compat.sendSessionIdHeader: false`, allowing strict OpenAI-compatible proxies to omit the underscore-containing `session_id` header while still sending other session-affinity headers ([#3579](https://github.com/badlogic/pi-mono/issues/3579))
|
||||
@@ -16,6 +18,7 @@
|
||||
- Fixed `openai-completions` streamed tool-call assembly to coalesce deltas by stable tool index when OpenAI-compatible gateways mutate tool call IDs mid-stream, preventing malformed Kimi K2.6/OpenCode tool streams from splitting one call into multiple bogus tool calls ([#3576](https://github.com/badlogic/pi-mono/issues/3576))
|
||||
- Fixed `packages/ai` E2E coverage to use currently supported OpenAI Responses and OpenAI Codex models, and updated the Bedrock adaptive-thinking payload expectation to match the current `display: "summarized"` shape.
|
||||
- Fixed built-in `kimi-coding` model generation to attach `User-Agent: KimiCLI/1.5` to all generated Kimi models, overriding the Anthropic SDK default UA so direct Kimi Coding requests use the provider's expected client identity ([#3586](https://github.com/badlogic/pi-mono/issues/3586))
|
||||
- Fixed GPT-5.5 Codex capability handling to clamp unsupported minimal reasoning to `low` and apply the model's 2.5x priority service-tier pricing multiplier ([#3618](https://github.com/badlogic/pi-mono/pull/3618) by [@markusylisiurunen](https://github.com/markusylisiurunen))
|
||||
|
||||
## [0.69.0] - 2026-04-22
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import {
|
||||
type GenerateContentConfig,
|
||||
type GenerateContentParameters,
|
||||
GoogleGenAI,
|
||||
type HttpOptions,
|
||||
ResourceScope,
|
||||
type ThinkingConfig,
|
||||
ThinkingLevel,
|
||||
} from "@google/genai";
|
||||
@@ -331,20 +333,12 @@ function createClient(
|
||||
location: string,
|
||||
optionsHeaders?: Record<string, string>,
|
||||
): GoogleGenAI {
|
||||
const httpOptions: { headers?: Record<string, string> } = {};
|
||||
|
||||
if (model.headers || optionsHeaders) {
|
||||
httpOptions.headers = { ...model.headers, ...optionsHeaders };
|
||||
}
|
||||
|
||||
const hasHttpOptions = Object.values(httpOptions).some(Boolean);
|
||||
|
||||
return new GoogleGenAI({
|
||||
vertexai: true,
|
||||
project,
|
||||
location,
|
||||
apiVersion: API_VERSION,
|
||||
httpOptions: hasHttpOptions ? httpOptions : undefined,
|
||||
httpOptions: buildHttpOptions(model, optionsHeaders),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -353,20 +347,50 @@ function createClientWithApiKey(
|
||||
apiKey: string,
|
||||
optionsHeaders?: Record<string, string>,
|
||||
): GoogleGenAI {
|
||||
const httpOptions: { headers?: Record<string, string> } = {};
|
||||
return new GoogleGenAI({
|
||||
vertexai: true,
|
||||
apiKey,
|
||||
apiVersion: API_VERSION,
|
||||
httpOptions: buildHttpOptions(model, optionsHeaders),
|
||||
});
|
||||
}
|
||||
|
||||
function buildHttpOptions(
|
||||
model: Model<"google-vertex">,
|
||||
optionsHeaders?: Record<string, string>,
|
||||
): HttpOptions | undefined {
|
||||
const httpOptions: HttpOptions = {};
|
||||
const baseUrl = resolveCustomBaseUrl(model.baseUrl);
|
||||
if (baseUrl) {
|
||||
httpOptions.baseUrl = baseUrl;
|
||||
httpOptions.baseUrlResourceScope = ResourceScope.COLLECTION;
|
||||
if (baseUrlIncludesApiVersion(baseUrl)) {
|
||||
httpOptions.apiVersion = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (model.headers || optionsHeaders) {
|
||||
httpOptions.headers = { ...model.headers, ...optionsHeaders };
|
||||
}
|
||||
|
||||
const hasHttpOptions = Object.values(httpOptions).some(Boolean);
|
||||
return Object.keys(httpOptions).length > 0 ? httpOptions : undefined;
|
||||
}
|
||||
|
||||
return new GoogleGenAI({
|
||||
vertexai: true,
|
||||
apiKey,
|
||||
apiVersion: API_VERSION,
|
||||
httpOptions: hasHttpOptions ? httpOptions : undefined,
|
||||
});
|
||||
function resolveCustomBaseUrl(baseUrl: string): string | undefined {
|
||||
const trimmed = baseUrl.trim();
|
||||
if (!trimmed || trimmed.includes("{location}")) {
|
||||
return undefined;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function baseUrlIncludesApiVersion(baseUrl: string): boolean {
|
||||
try {
|
||||
const url = new URL(baseUrl);
|
||||
return url.pathname.split("/").some((part) => /^v\d+(?:beta\d*)?$/.test(part));
|
||||
} catch {
|
||||
return /(?:^|\/)v\d+(?:beta\d*)?(?:\/|$)/.test(baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveApiKey(options?: GoogleVertexOptions): string | undefined {
|
||||
|
||||
@@ -32,6 +32,9 @@ vi.mock("@google/genai", () => {
|
||||
|
||||
return {
|
||||
GoogleGenAI,
|
||||
ResourceScope: {
|
||||
COLLECTION: "COLLECTION",
|
||||
},
|
||||
ThinkingLevel: {
|
||||
THINKING_LEVEL_UNSPECIFIED: "THINKING_LEVEL_UNSPECIFIED",
|
||||
MINIMAL: "MINIMAL",
|
||||
@@ -44,7 +47,7 @@ vi.mock("@google/genai", () => {
|
||||
|
||||
import { getModel } from "../src/models.js";
|
||||
import { streamGoogleVertex } from "../src/providers/google-vertex.js";
|
||||
import type { Context } from "../src/types.js";
|
||||
import type { Context, Model } from "../src/types.js";
|
||||
|
||||
const model = getModel("google-vertex", "gemini-3-flash-preview");
|
||||
const context: Context = {
|
||||
@@ -141,4 +144,80 @@ describe("google-vertex api key resolution", () => {
|
||||
expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("project");
|
||||
expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("location");
|
||||
});
|
||||
|
||||
it("does not forward generated Vertex base URL placeholders", async () => {
|
||||
const stream = streamGoogleVertex(model, context, {
|
||||
project: "test-project",
|
||||
location: "us-central1",
|
||||
});
|
||||
|
||||
await stream.result();
|
||||
|
||||
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
||||
expect(googleGenAiMock.constructorCalls[0]?.httpOptions).toBeUndefined();
|
||||
});
|
||||
|
||||
it("forwards custom baseUrl to the ADC client", async () => {
|
||||
const customModel: Model<"google-vertex"> = { ...model, baseUrl: "https://proxy.example.com" };
|
||||
const stream = streamGoogleVertex(customModel, context, {
|
||||
project: "test-project",
|
||||
location: "us-central1",
|
||||
});
|
||||
|
||||
await stream.result();
|
||||
|
||||
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
||||
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
|
||||
vertexai: true,
|
||||
project: "test-project",
|
||||
location: "us-central1",
|
||||
apiVersion: "v1",
|
||||
httpOptions: {
|
||||
baseUrl: "https://proxy.example.com",
|
||||
baseUrlResourceScope: "COLLECTION",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards custom baseUrl to the API key client", async () => {
|
||||
const customModel: Model<"google-vertex"> = { ...model, baseUrl: "https://proxy.example.com" };
|
||||
const stream = streamGoogleVertex(customModel, context, {
|
||||
apiKey: "AIzaSyExampleRealisticLookingApiKey123456",
|
||||
});
|
||||
|
||||
await stream.result();
|
||||
|
||||
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
||||
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
|
||||
vertexai: true,
|
||||
apiKey: "AIzaSyExampleRealisticLookingApiKey123456",
|
||||
apiVersion: "v1",
|
||||
httpOptions: {
|
||||
baseUrl: "https://proxy.example.com",
|
||||
baseUrlResourceScope: "COLLECTION",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not append apiVersion when custom baseUrl already includes one", async () => {
|
||||
const customModel: Model<"google-vertex"> = {
|
||||
...model,
|
||||
baseUrl: "https://proxy.example.com/v1/projects/test-project/locations/global",
|
||||
};
|
||||
const stream = streamGoogleVertex(customModel, context, {
|
||||
project: "test-project",
|
||||
location: "us-central1",
|
||||
});
|
||||
|
||||
await stream.result();
|
||||
|
||||
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
||||
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
|
||||
httpOptions: {
|
||||
baseUrl: "https://proxy.example.com/v1/projects/test-project/locations/global",
|
||||
baseUrlResourceScope: "COLLECTION",
|
||||
apiVersion: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user