mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-06 15:18:58 +00:00
feat(openai): support chat-latest override
This commit is contained in:
@@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { buildOpenAIProvider } from "./openai-provider.js";
|
||||
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "";
|
||||
const DEFAULT_LIVE_MODEL_IDS = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4-nano"] as const;
|
||||
const DEFAULT_LIVE_MODEL_IDS = ["chat-latest", "gpt-5.5", "gpt-5.4-mini", "gpt-5.4-nano"] as const;
|
||||
const liveEnabled = OPENAI_API_KEY.trim().length > 0 && process.env.OPENCLAW_LIVE_TEST === "1";
|
||||
const describeLive = liveEnabled ? describe : describe.skip;
|
||||
|
||||
@@ -16,6 +16,7 @@ type LiveModelCase = {
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
reasoning: boolean;
|
||||
};
|
||||
|
||||
function findOpenAIModel(modelId: string): Model<Api> | null {
|
||||
@@ -24,6 +25,16 @@ function findOpenAIModel(modelId: string): Model<Api> | null {
|
||||
|
||||
function resolveLiveModelCase(modelId: string): LiveModelCase {
|
||||
switch (modelId) {
|
||||
case "chat-latest":
|
||||
return {
|
||||
modelId,
|
||||
templateId: "gpt-5.5",
|
||||
templateName: "GPT-5.5",
|
||||
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
reasoning: false,
|
||||
};
|
||||
case "gpt-5.5":
|
||||
return {
|
||||
modelId,
|
||||
@@ -32,6 +43,7 @@ function resolveLiveModelCase(modelId: string): LiveModelCase {
|
||||
cost: { input: 5, output: 30, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
reasoning: true,
|
||||
};
|
||||
case "gpt-5.5-pro":
|
||||
return {
|
||||
@@ -41,6 +53,7 @@ function resolveLiveModelCase(modelId: string): LiveModelCase {
|
||||
cost: { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
reasoning: true,
|
||||
};
|
||||
case "gpt-5.4":
|
||||
return {
|
||||
@@ -50,6 +63,7 @@ function resolveLiveModelCase(modelId: string): LiveModelCase {
|
||||
cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
reasoning: true,
|
||||
};
|
||||
case "gpt-5.4-pro":
|
||||
return {
|
||||
@@ -59,6 +73,7 @@ function resolveLiveModelCase(modelId: string): LiveModelCase {
|
||||
cost: { input: 21, output: 168, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
reasoning: true,
|
||||
};
|
||||
case "gpt-5.4-mini":
|
||||
return {
|
||||
@@ -68,6 +83,7 @@ function resolveLiveModelCase(modelId: string): LiveModelCase {
|
||||
cost: { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0 },
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
reasoning: true,
|
||||
};
|
||||
case "gpt-5.4-nano":
|
||||
return {
|
||||
@@ -77,6 +93,7 @@ function resolveLiveModelCase(modelId: string): LiveModelCase {
|
||||
cost: { input: 0.05, output: 0.4, cacheRead: 0.005, cacheWrite: 0 },
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
reasoning: true,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unsupported live OpenAI model: ${modelId}`);
|
||||
@@ -113,7 +130,7 @@ describeLive("buildOpenAIProvider live", () => {
|
||||
provider: "openai",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: true,
|
||||
reasoning: liveCase.reasoning,
|
||||
input: ["text", "image"],
|
||||
cost: liveCase.cost,
|
||||
contextWindow: liveCase.contextWindow,
|
||||
@@ -146,6 +163,7 @@ describeLive("buildOpenAIProvider live", () => {
|
||||
id: liveCase.modelId,
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: liveCase.reasoning,
|
||||
});
|
||||
|
||||
const client = new OpenAI({
|
||||
@@ -158,7 +176,7 @@ describeLive("buildOpenAIProvider live", () => {
|
||||
instructions: "Return exactly OK and no other text.",
|
||||
input: "Return exactly OK.",
|
||||
max_output_tokens: 64,
|
||||
reasoning: { effort: "none" },
|
||||
...(liveCase.reasoning ? { reasoning: { effort: "none" as const } } : {}),
|
||||
text: { verbosity: "low" },
|
||||
});
|
||||
|
||||
|
||||
@@ -282,6 +282,60 @@ describe("buildOpenAIProvider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves chat-latest as an explicit direct API model override", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
|
||||
const model = provider.resolveDynamicModel?.({
|
||||
provider: "openai",
|
||||
modelId: "chat-latest",
|
||||
modelRegistry: {
|
||||
find: (_provider: string, id: string) =>
|
||||
id === "gpt-5.5"
|
||||
? {
|
||||
id,
|
||||
name: "GPT-5.5",
|
||||
provider: "openai",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
||||
contextWindow: 1_050_000,
|
||||
maxTokens: 128_000,
|
||||
}
|
||||
: null,
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(model).toMatchObject({
|
||||
provider: "openai",
|
||||
id: "chat-latest",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
||||
});
|
||||
|
||||
const fallback = provider.resolveDynamicModel?.({
|
||||
provider: "openai",
|
||||
modelId: "chat-latest",
|
||||
modelRegistry: { find: () => null },
|
||||
} as never);
|
||||
|
||||
expect(fallback).toMatchObject({
|
||||
provider: "openai",
|
||||
id: "chat-latest",
|
||||
api: "openai-responses",
|
||||
reasoning: false,
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves gpt-5.5 to Pi and resolves gpt-5.5-pro locally", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
|
||||
@@ -340,7 +394,7 @@ describe("buildOpenAIProvider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces gpt-5.5 in xhigh without synthetic catalog metadata", () => {
|
||||
it("keeps chat-latest and gpt-5.5 out of synthetic catalog metadata", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
|
||||
expect(
|
||||
@@ -363,6 +417,12 @@ describe("buildOpenAIProvider", () => {
|
||||
id: "gpt-5.5",
|
||||
}),
|
||||
);
|
||||
expect(entries).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
provider: "openai",
|
||||
id: "chat-latest",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps modern live selection on OpenAI 5.2+ and current Codex models", () => {
|
||||
@@ -387,6 +447,12 @@ describe("buildOpenAIProvider", () => {
|
||||
modelId: "gpt-5.4",
|
||||
} as never),
|
||||
).toBe(true);
|
||||
expect(
|
||||
provider.isModernModelRef?.({
|
||||
provider: "openai",
|
||||
modelId: "chat-latest",
|
||||
} as never),
|
||||
).toBe(true);
|
||||
expect(
|
||||
provider.isModernModelRef?.({
|
||||
provider: "openai",
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { resolveOpenAIThinkingProfile } from "./thinking-policy.js";
|
||||
|
||||
const PROVIDER_ID = "openai";
|
||||
const OPENAI_CHAT_LATEST_MODEL_ID = "chat-latest";
|
||||
const OPENAI_GPT_55_MODEL_ID = "gpt-5.5";
|
||||
const OPENAI_GPT_55_PRO_MODEL_ID = "gpt-5.5-pro";
|
||||
const OPENAI_GPT_54_MODEL_ID = "gpt-5.4";
|
||||
@@ -35,6 +36,7 @@ const OPENAI_GPT_54_PRO_CONTEXT_TOKENS = 1_050_000;
|
||||
const OPENAI_GPT_54_MINI_CONTEXT_TOKENS = 400_000;
|
||||
const OPENAI_GPT_54_NANO_CONTEXT_TOKENS = 400_000;
|
||||
const OPENAI_GPT_54_MAX_TOKENS = 128_000;
|
||||
const OPENAI_CHAT_LATEST_COST = { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 } as const;
|
||||
const OPENAI_GPT_55_PRO_COST = { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 } as const;
|
||||
const OPENAI_GPT_54_COST = { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 } as const;
|
||||
const OPENAI_GPT_54_PRO_COST = { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 } as const;
|
||||
@@ -60,7 +62,13 @@ const OPENAI_GPT_54_TEMPLATE_MODEL_IDS = ["gpt-5.2"] as const;
|
||||
const OPENAI_GPT_54_PRO_TEMPLATE_MODEL_IDS = ["gpt-5.2-pro", "gpt-5.2"] as const;
|
||||
const OPENAI_GPT_54_MINI_TEMPLATE_MODEL_IDS = ["gpt-5-mini"] as const;
|
||||
const OPENAI_GPT_54_NANO_TEMPLATE_MODEL_IDS = ["gpt-5-nano", "gpt-5-mini"] as const;
|
||||
const OPENAI_CHAT_LATEST_TEMPLATE_MODEL_IDS = [
|
||||
OPENAI_GPT_55_MODEL_ID,
|
||||
OPENAI_GPT_54_MODEL_ID,
|
||||
"gpt-5.2",
|
||||
] as const;
|
||||
const OPENAI_MODERN_MODEL_IDS = [
|
||||
OPENAI_CHAT_LATEST_MODEL_ID,
|
||||
OPENAI_GPT_55_MODEL_ID,
|
||||
OPENAI_GPT_55_PRO_MODEL_ID,
|
||||
OPENAI_GPT_54_MODEL_ID,
|
||||
@@ -106,7 +114,19 @@ function resolveOpenAIGptForwardCompatModel(ctx: ProviderResolveDynamicModelCont
|
||||
const lower = normalizeLowercaseStringOrEmpty(trimmedModelId);
|
||||
let templateIds: readonly string[];
|
||||
let patch: Partial<ProviderRuntimeModel>;
|
||||
if (lower === OPENAI_GPT_55_PRO_MODEL_ID) {
|
||||
if (lower === OPENAI_CHAT_LATEST_MODEL_ID) {
|
||||
templateIds = OPENAI_CHAT_LATEST_TEMPLATE_MODEL_IDS;
|
||||
patch = {
|
||||
api: "openai-responses",
|
||||
provider: PROVIDER_ID,
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: OPENAI_CHAT_LATEST_COST,
|
||||
contextWindow: 400_000,
|
||||
maxTokens: OPENAI_GPT_54_MAX_TOKENS,
|
||||
};
|
||||
} else if (lower === OPENAI_GPT_55_PRO_MODEL_ID) {
|
||||
templateIds = OPENAI_GPT_55_PRO_TEMPLATE_MODEL_IDS;
|
||||
patch = {
|
||||
api: "openai-responses",
|
||||
@@ -182,7 +202,7 @@ function resolveOpenAIGptForwardCompatModel(ctx: ProviderResolveDynamicModelCont
|
||||
id: trimmedModelId,
|
||||
name: trimmedModelId,
|
||||
...patch,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
cost: patch.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: patch.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
|
||||
maxTokens: patch.maxTokens ?? DEFAULT_CONTEXT_TOKENS,
|
||||
} as ProviderRuntimeModel)
|
||||
|
||||
Reference in New Issue
Block a user