@@ -5,9 +5,11 @@
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added GPT-5.5 to OpenAI Codex model generation.
|
- 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
|
||||||
|
|
||||||
|
- 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 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 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))
|
- 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 `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 `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 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
|
## [0.69.0] - 2026-04-22
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import {
|
|||||||
type GenerateContentConfig,
|
type GenerateContentConfig,
|
||||||
type GenerateContentParameters,
|
type GenerateContentParameters,
|
||||||
GoogleGenAI,
|
GoogleGenAI,
|
||||||
|
type HttpOptions,
|
||||||
|
ResourceScope,
|
||||||
type ThinkingConfig,
|
type ThinkingConfig,
|
||||||
ThinkingLevel,
|
ThinkingLevel,
|
||||||
} from "@google/genai";
|
} from "@google/genai";
|
||||||
@@ -331,20 +333,12 @@ function createClient(
|
|||||||
location: string,
|
location: string,
|
||||||
optionsHeaders?: Record<string, string>,
|
optionsHeaders?: Record<string, string>,
|
||||||
): GoogleGenAI {
|
): 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({
|
return new GoogleGenAI({
|
||||||
vertexai: true,
|
vertexai: true,
|
||||||
project,
|
project,
|
||||||
location,
|
location,
|
||||||
apiVersion: API_VERSION,
|
apiVersion: API_VERSION,
|
||||||
httpOptions: hasHttpOptions ? httpOptions : undefined,
|
httpOptions: buildHttpOptions(model, optionsHeaders),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,20 +347,50 @@ function createClientWithApiKey(
|
|||||||
apiKey: string,
|
apiKey: string,
|
||||||
optionsHeaders?: Record<string, string>,
|
optionsHeaders?: Record<string, string>,
|
||||||
): GoogleGenAI {
|
): 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) {
|
if (model.headers || optionsHeaders) {
|
||||||
httpOptions.headers = { ...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({
|
function resolveCustomBaseUrl(baseUrl: string): string | undefined {
|
||||||
vertexai: true,
|
const trimmed = baseUrl.trim();
|
||||||
apiKey,
|
if (!trimmed || trimmed.includes("{location}")) {
|
||||||
apiVersion: API_VERSION,
|
return undefined;
|
||||||
httpOptions: hasHttpOptions ? httpOptions : 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 {
|
function resolveApiKey(options?: GoogleVertexOptions): string | undefined {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ vi.mock("@google/genai", () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
GoogleGenAI,
|
GoogleGenAI,
|
||||||
|
ResourceScope: {
|
||||||
|
COLLECTION: "COLLECTION",
|
||||||
|
},
|
||||||
ThinkingLevel: {
|
ThinkingLevel: {
|
||||||
THINKING_LEVEL_UNSPECIFIED: "THINKING_LEVEL_UNSPECIFIED",
|
THINKING_LEVEL_UNSPECIFIED: "THINKING_LEVEL_UNSPECIFIED",
|
||||||
MINIMAL: "MINIMAL",
|
MINIMAL: "MINIMAL",
|
||||||
@@ -44,7 +47,7 @@ vi.mock("@google/genai", () => {
|
|||||||
|
|
||||||
import { getModel } from "../src/models.js";
|
import { getModel } from "../src/models.js";
|
||||||
import { streamGoogleVertex } from "../src/providers/google-vertex.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 model = getModel("google-vertex", "gemini-3-flash-preview");
|
||||||
const context: Context = {
|
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("project");
|
||||||
expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("location");
|
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