diff --git a/next.config.mjs b/next.config.mjs index 18a2be156d..fc6a4d2d6c 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -14,6 +14,11 @@ const nextConfig = { }, }, output: "standalone", + outputFileTracingExcludes: { + // Planning/task docs are not runtime assets and can break standalone copies + // when broad fs/path tracing pulls the whole repository into the NFT graph. + "/*": ["./_tasks/**/*"], + }, serverExternalPackages: [ "pino", "pino-pretty", diff --git a/open-sse/config/anthropicHeaders.ts b/open-sse/config/anthropicHeaders.ts new file mode 100644 index 0000000000..efb3986bf8 --- /dev/null +++ b/open-sse/config/anthropicHeaders.ts @@ -0,0 +1,32 @@ +export const ANTHROPIC_VERSION_HEADER = "2023-06-01"; + +const ANTHROPIC_BETA_BASE = Object.freeze([ + "claude-code-20250219", + "oauth-2025-04-20", + "interleaved-thinking-2025-05-14", + "context-management-2025-06-27", + "prompt-caching-scope-2026-01-05", + "advanced-tool-use-2025-11-20", + "effort-2025-11-24", + "structured-outputs-2025-12-15", + "fast-mode-2026-02-01", + "redact-thinking-2026-02-12", + "token-efficient-tools-2026-03-28", +]); + +const CLAUDE_OAUTH_EXTRA_BETAS = Object.freeze(["fine-grained-tool-streaming-2025-05-14"]); + +export const ANTHROPIC_BETA_FULL = ANTHROPIC_BETA_BASE.join(","); +export const ANTHROPIC_BETA_API_KEY = ANTHROPIC_BETA_BASE.filter( + (beta) => beta !== "oauth-2025-04-20" +).join(","); +export const ANTHROPIC_BETA_CLAUDE_OAUTH = [ + ...ANTHROPIC_BETA_BASE.slice(0, 3), + ...CLAUDE_OAUTH_EXTRA_BETAS, + ...ANTHROPIC_BETA_BASE.slice(3), +].join(","); + +export const CLAUDE_CLI_VERSION = "2.1.92"; +export const CLAUDE_CLI_USER_AGENT = `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`; +export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.80.0"; +export const CLAUDE_CLI_STAINLESS_RUNTIME_VERSION = "v24.14.0"; diff --git a/open-sse/config/antigravityModelAliases.ts b/open-sse/config/antigravityModelAliases.ts new file mode 100644 index 0000000000..4d6f748766 --- /dev/null +++ b/open-sse/config/antigravityModelAliases.ts @@ -0,0 +1,48 @@ +export const ANTIGRAVITY_MODEL_ALIASES = Object.freeze({ + "gemini-3-pro-preview": "gemini-3.1-pro-high", + "gemini-3-flash-preview": "gemini-3-flash", + "gemini-3-pro-image-preview": "gemini-3-pro-image", + "gemini-2.5-computer-use-preview-10-2025": "rev19-uic3-1p", + "gemini-claude-sonnet-4-5": "claude-sonnet-4-5", + "gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking", + "gemini-claude-opus-4-5-thinking": "claude-opus-4-5-thinking", +}); + +export const ANTIGRAVITY_REVERSE_MODEL_ALIASES = Object.freeze( + Object.entries(ANTIGRAVITY_MODEL_ALIASES).reduce>( + (acc, [alias, target]) => { + if (!acc[target]) { + acc[target] = alias; + } + return acc; + }, + {} + ) +); + +const CLIENT_VISIBLE_MODEL_NAMES = Object.freeze({ + "gemini-3-pro-preview": "Gemini 3 Pro Preview", + "gemini-3-flash-preview": "Gemini 3 Flash Preview", + "gemini-3-pro-image-preview": "Gemini 3 Pro Image Preview", + "gemini-2.5-computer-use-preview-10-2025": "Gemini 2.5 Computer Use Preview (10/2025)", + "gemini-claude-sonnet-4-5": "Claude Sonnet 4.5 (Gemini Route)", + "gemini-claude-sonnet-4-5-thinking": "Claude Sonnet 4.5 Thinking (Gemini Route)", + "gemini-claude-opus-4-5-thinking": "Claude Opus 4.5 Thinking (Gemini Route)", +}); + +export function resolveAntigravityModelId(modelId: string): string { + if (!modelId) return modelId; + return ANTIGRAVITY_MODEL_ALIASES[modelId] || modelId; +} + +export function toClientAntigravityModelId(modelId: string): string { + if (!modelId) return modelId; + return ANTIGRAVITY_REVERSE_MODEL_ALIASES[modelId] || modelId; +} + +export function getClientVisibleAntigravityModelName( + modelId: string, + fallbackName?: string +): string { + return CLIENT_VISIBLE_MODEL_NAMES[modelId] || fallbackName || modelId; +} diff --git a/open-sse/config/antigravityUpstream.ts b/open-sse/config/antigravityUpstream.ts index a4ca845301..e020723d15 100644 --- a/open-sse/config/antigravityUpstream.ts +++ b/open-sse/config/antigravityUpstream.ts @@ -1,7 +1,7 @@ export const ANTIGRAVITY_BASE_URLS = Object.freeze([ + "https://cloudcode-pa.googleapis.com", "https://daily-cloudcode-pa.googleapis.com", "https://daily-cloudcode-pa.sandbox.googleapis.com", - "https://cloudcode-pa.googleapis.com", ]); const ANTIGRAVITY_MODELS_PATH = "/v1internal:models"; diff --git a/open-sse/config/glmProvider.ts b/open-sse/config/glmProvider.ts index afef06a7e7..f74d537dbe 100644 --- a/open-sse/config/glmProvider.ts +++ b/open-sse/config/glmProvider.ts @@ -1,10 +1,12 @@ +import { ANTHROPIC_BETA_API_KEY, ANTHROPIC_VERSION_HEADER } from "./anthropicHeaders.ts"; + type JsonRecord = Record; export type GlmApiRegion = "international" | "china"; export const GLM_SHARED_HEADERS = Object.freeze({ - "Anthropic-Version": "2023-06-01", - "Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14", + "Anthropic-Version": ANTHROPIC_VERSION_HEADER, + "Anthropic-Beta": ANTHROPIC_BETA_API_KEY, }); export const GLM_SHARED_MODELS = Object.freeze([ diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index 6ac8cb6eec..f5df635ec5 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -5,36 +5,71 @@ * Each provider has its own request format and endpoint. */ -const IMAGE_MODEL_ALIASES = { +interface ImageModelEntry { + id: string; + name: string; + inputModalities?: string[]; + description?: string; +} + +interface ImageProviderConfig { + id: string; + baseUrl: string; + fallbackUrl?: string; + proUrl?: string; + statusUrl?: string; + alias?: string; + authType: string; + authHeader: string; + format: string; + models: ImageModelEntry[]; + supportedSizes: string[]; +} + +interface ImageModelAliasEntry { + provider: string; + model: string; + name: string; + listInCatalog: boolean; + inputModalities?: string[]; + description?: string; +} + +const IMAGE_MODEL_ALIASES: Record = { "flux-kontext": { provider: "pollinations", model: "flux-kontext", name: "FLUX Kontext", listInCatalog: true, + inputModalities: ["text", "image"], }, "flux-kontext-max": { provider: "together", model: "black-forest-labs/FLUX.1.1-pro", name: "FLUX Kontext Max", listInCatalog: true, + inputModalities: ["text", "image"], }, "flux-redux": { provider: "together", model: "black-forest-labs/FLUX.1-redux", name: "FLUX Redux", listInCatalog: true, + inputModalities: ["text", "image"], }, "flux-depth": { provider: "together", model: "black-forest-labs/FLUX.1-depth", name: "FLUX Depth", listInCatalog: true, + inputModalities: ["text", "image"], }, "flux-canny": { provider: "together", model: "black-forest-labs/FLUX.1-canny", name: "FLUX Canny", listInCatalog: true, + inputModalities: ["text", "image"], }, "flux-dev-lora": { provider: "together", @@ -67,7 +102,7 @@ function findImageModelConfig(providerId, modelId) { return provider.models.find((model) => model.id === modelId) || null; } -export const IMAGE_PROVIDERS = { +export const IMAGE_PROVIDERS: Record = { openai: { id: "openai", baseUrl: "https://api.openai.com/v1/images/generations", @@ -99,12 +134,32 @@ export const IMAGE_PROVIDERS = { authHeader: "bearer", format: "openai", models: [ - { id: "black-forest-labs/FLUX.1.1-pro", name: "FLUX 1.1 Pro" }, + { + id: "black-forest-labs/FLUX.1.1-pro", + name: "FLUX 1.1 Pro", + inputModalities: ["text", "image"], + description: "Advanced contextual image editing and image-to-image generation", + }, { id: "black-forest-labs/FLUX.1-schnell-Free", name: "FLUX 1 Schnell (Free)" }, { id: "stabilityai/stable-diffusion-xl-base-1.0", name: "SDXL Base 1.0" }, - { id: "black-forest-labs/FLUX.1-redux", name: "FLUX.1 Redux" }, - { id: "black-forest-labs/FLUX.1-depth", name: "FLUX.1 Depth" }, - { id: "black-forest-labs/FLUX.1-canny", name: "FLUX.1 Canny" }, + { + id: "black-forest-labs/FLUX.1-redux", + name: "FLUX.1 Redux", + inputModalities: ["text", "image"], + description: "Generate prompt-guided variations from an input image", + }, + { + id: "black-forest-labs/FLUX.1-depth", + name: "FLUX.1 Depth", + inputModalities: ["text", "image"], + description: "Depth-conditioned image generation from an input image", + }, + { + id: "black-forest-labs/FLUX.1-canny", + name: "FLUX.1 Canny", + inputModalities: ["text", "image"], + description: "Canny edge guided image generation from an input image", + }, { id: "black-forest-labs/FLUX.1-dev-lora", name: "FLUX.1 Dev LoRA" }, ], supportedSizes: ["1024x1024", "512x512"], @@ -231,8 +286,18 @@ export const IMAGE_PROVIDERS = { { id: "gptimage", name: "GPT Image 1 Mini" }, { id: "qwen-image", name: "Qwen Image Plus" }, { id: "wan-image", name: "Wan 2.7 Image" }, - { id: "flux-kontext", name: "FLUX.1 Kontext" }, - { id: "flux-kontext-max", name: "FLUX.1 Kontext Max" }, + { + id: "flux-kontext", + name: "FLUX.1 Kontext", + inputModalities: ["text", "image"], + description: "Context-aware image editing with optional source image", + }, + { + id: "flux-kontext-max", + name: "FLUX.1 Kontext Max", + inputModalities: ["text", "image"], + description: "Higher quality Kontext editing with optional source image", + }, { id: "gptimage-large", name: "GPT Image 1.5" }, ], supportedSizes: ["1024x1024", "512x512"], @@ -277,20 +342,24 @@ export const IMAGE_PROVIDERS = { { id: "sd3.5-medium", name: "sd3.5-medium" }, { id: "stable-image-ultra", name: "Stable Image Ultra" }, { id: "stable-image-core", name: "Stable Image Core" }, - { id: "inpaint", name: "Inpaint" }, - { id: "outpaint", name: "Outpaint" }, - { id: "erase", name: "Erase" }, - { id: "search-and-replace", name: "Search and Replace" }, - { id: "search-and-recolor", name: "Search and Recolor" }, - { id: "remove-background", name: "Remove Background" }, - { id: "replace-background-and-relight", name: "Replace Background and Relight" }, - { id: "fast", name: "Fast Upscale" }, - { id: "conservative", name: "Conservative Upscale" }, - { id: "creative", name: "Creative Upscale" }, - { id: "sketch", name: "Sketch Control" }, - { id: "structure", name: "Structure Control" }, - { id: "style", name: "Style Control" }, - { id: "style-transfer", name: "Style Transfer" }, + { id: "inpaint", name: "Inpaint", inputModalities: ["text", "image"] }, + { id: "outpaint", name: "Outpaint", inputModalities: ["text", "image"] }, + { id: "erase", name: "Erase", inputModalities: ["image"] }, + { id: "search-and-replace", name: "Search and Replace", inputModalities: ["text", "image"] }, + { id: "search-and-recolor", name: "Search and Recolor", inputModalities: ["text", "image"] }, + { id: "remove-background", name: "Remove Background", inputModalities: ["image"] }, + { + id: "replace-background-and-relight", + name: "Replace Background and Relight", + inputModalities: ["text", "image"], + }, + { id: "fast", name: "Fast Upscale", inputModalities: ["image"] }, + { id: "conservative", name: "Conservative Upscale", inputModalities: ["image"] }, + { id: "creative", name: "Creative Upscale", inputModalities: ["text", "image"] }, + { id: "sketch", name: "Sketch Control", inputModalities: ["text", "image"] }, + { id: "structure", name: "Structure Control", inputModalities: ["text", "image"] }, + { id: "style", name: "Style Control", inputModalities: ["text", "image"] }, + { id: "style-transfer", name: "Style Transfer", inputModalities: ["text", "image"] }, ], supportedSizes: ["1024x1024", "1024x1280", "1280x1024"], }, @@ -302,10 +371,14 @@ export const IMAGE_PROVIDERS = { authHeader: "x-key", format: "black-forest-labs", models: [ - { id: "flux-kontext-pro", name: "flux-kontext-pro" }, - { id: "flux-kontext-max", name: "flux-kontext-max" }, - { id: "flux-pro-1.0-fill", name: "flux-pro-1.0-fill" }, - { id: "flux-pro-1.0-expand", name: "flux-pro-1.0-expand" }, + { id: "flux-kontext-pro", name: "flux-kontext-pro", inputModalities: ["text", "image"] }, + { id: "flux-kontext-max", name: "flux-kontext-max", inputModalities: ["text", "image"] }, + { id: "flux-pro-1.0-fill", name: "flux-pro-1.0-fill", inputModalities: ["text", "image"] }, + { + id: "flux-pro-1.0-expand", + name: "flux-pro-1.0-expand", + inputModalities: ["text", "image"], + }, { id: "flux-pro-1.1", name: "flux-pro-1.1" }, { id: "flux-pro-1.1-ultra", name: "flux-pro-1.1-ultra" }, { id: "flux-dev", name: "flux-dev" }, @@ -333,7 +406,7 @@ export const IMAGE_PROVIDERS = { authType: "apikey", authHeader: "x-api-key", format: "topaz", - models: [{ id: "topaz-enhance", name: "topaz-enhance" }], + models: [{ id: "topaz-enhance", name: "topaz-enhance", inputModalities: ["image"] }], supportedSizes: ["1024x1024"], }, }; @@ -396,6 +469,8 @@ export function getAllImageModels() { name: model.name, provider: providerId, supportedSizes: config.supportedSizes, + inputModalities: model.inputModalities || ["text"], + description: model.description || undefined, }); } } @@ -408,6 +483,8 @@ export function getAllImageModels() { name: target.name || modelConfig?.name || alias, provider: target.provider, supportedSizes: providerConfig?.supportedSizes || [], + inputModalities: target.inputModalities || modelConfig?.inputModalities || ["text"], + description: target.description || modelConfig?.description || undefined, }); } return models; diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 02369d2eb5..162d907749 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -8,6 +8,14 @@ import { platform, arch } from "os"; import { ANTIGRAVITY_BASE_URLS } from "./antigravityUpstream.ts"; +import { + ANTHROPIC_BETA_API_KEY, + ANTHROPIC_BETA_CLAUDE_OAUTH, + ANTHROPIC_VERSION_HEADER, + CLAUDE_CLI_STAINLESS_PACKAGE_VERSION, + CLAUDE_CLI_STAINLESS_RUNTIME_VERSION, + CLAUDE_CLI_USER_AGENT, +} from "./anthropicHeaders.ts"; import { getCodexDefaultHeaders } from "./codexClient.ts"; import { GLMT_REQUEST_DEFAULTS, @@ -108,8 +116,8 @@ const KIMI_CODING_SHARED = { baseUrl: "https://api.kimi.com/coding/v1/messages", authHeader: "x-api-key", headers: { - "Anthropic-Version": "2023-06-01", - "Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14", + "Anthropic-Version": ANTHROPIC_VERSION_HEADER, + "Anthropic-Beta": ANTHROPIC_BETA_API_KEY, }, models: [ { id: "kimi-k2.5", name: "Kimi K2.5" }, @@ -255,16 +263,15 @@ export const REGISTRY: Record = { authHeader: "x-api-key", defaultContextLength: 200000, headers: { - "Anthropic-Version": "2023-06-01", - "Anthropic-Beta": - "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05", + "Anthropic-Version": ANTHROPIC_VERSION_HEADER, + "Anthropic-Beta": ANTHROPIC_BETA_CLAUDE_OAUTH, "Anthropic-Dangerous-Direct-Browser-Access": "true", - "User-Agent": "claude-cli/2.1.63 (external, cli)", + "User-Agent": CLAUDE_CLI_USER_AGENT, "X-App": "cli", "X-Stainless-Helper-Method": "stream", "X-Stainless-Retry-Count": "0", - "X-Stainless-Runtime-Version": "v24.3.0", - "X-Stainless-Package-Version": "0.74.0", + "X-Stainless-Runtime-Version": CLAUDE_CLI_STAINLESS_RUNTIME_VERSION, + "X-Stainless-Package-Version": CLAUDE_CLI_STAINLESS_PACKAGE_VERSION, "X-Stainless-Runtime": "node", "X-Stainless-Lang": "js", "X-Stainless-Arch": mapStainlessArch(), @@ -744,8 +751,8 @@ export const REGISTRY: Record = { authType: "apikey", authHeader: "x-api-key", headers: { - "Anthropic-Version": "2023-06-01", - "Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14", + "Anthropic-Version": ANTHROPIC_VERSION_HEADER, + "Anthropic-Beta": ANTHROPIC_BETA_API_KEY, }, models: [ { id: "qwen3.5-plus", name: "Qwen3.5 Plus" }, @@ -769,8 +776,8 @@ export const REGISTRY: Record = { authType: "apikey", authHeader: "x-api-key", headers: { - "Anthropic-Version": "2023-06-01", - "Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14", + "Anthropic-Version": ANTHROPIC_VERSION_HEADER, + "Anthropic-Beta": ANTHROPIC_BETA_API_KEY, }, models: [ { id: "glm-5", name: "GLM 5" }, @@ -901,8 +908,8 @@ export const REGISTRY: Record = { authType: "apikey", authHeader: "bearer", headers: { - "Anthropic-Version": "2023-06-01", - "Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14", + "Anthropic-Version": ANTHROPIC_VERSION_HEADER, + "Anthropic-Beta": ANTHROPIC_BETA_API_KEY, }, models: [ // T12/T28: MiniMax default upgraded from M2.5 to M2.7 @@ -925,8 +932,8 @@ export const REGISTRY: Record = { authType: "apikey", authHeader: "bearer", headers: { - "Anthropic-Version": "2023-06-01", - "Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14", + "Anthropic-Version": ANTHROPIC_VERSION_HEADER, + "Anthropic-Beta": ANTHROPIC_BETA_API_KEY, }, models: [ // Keep parity with minimax to ensure model discovery works for minimax-cn connections. diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index 83eeabe720..0fdb0f9a8e 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -1,8 +1,8 @@ import crypto, { randomUUID } from "crypto"; -import { BaseExecutor, mergeUpstreamExtraHeaders } from "./base.ts"; +import { BaseExecutor, mergeUpstreamExtraHeaders, type ExecuteInput } from "./base.ts"; import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.ts"; import { scrubProxyAndFingerprintHeaders } from "../services/antigravityHeaderScrub.ts"; -import { antigravityUserAgent, googApiClientHeader } from "../services/antigravityHeaders.ts"; +import { antigravityUserAgent } from "../services/antigravityHeaders.ts"; import { classify429, decide429, type Decision } from "../services/antigravity429Engine.ts"; import { injectCreditsField, @@ -13,6 +13,8 @@ import { } from "../services/antigravityCredits.ts"; import { persistCreditBalance, getAllPersistedCreditBalances } from "@/lib/db/creditBalance"; import { obfuscateSensitiveWords } from "../services/antigravityObfuscation.ts"; +import { resolveAntigravityVersion } from "../services/antigravityVersion.ts"; +import { resolveAntigravityModelId } from "../config/antigravityModelAliases.ts"; const MAX_RETRY_AFTER_MS = 60_000; const LONG_RETRY_THRESHOLD_MS = 60_000; @@ -91,6 +93,7 @@ function markCreditsExhausted(accountId: string): void { function cleanModelName(model: string): string { if (!model) return model; let clean = model.includes("/") ? model.split("/").pop()! : model; + clean = resolveAntigravityModelId(clean); // Normalize bare Pro IDs to the Low tier (matching OpenClaw convention). // The upstream API requires an explicit tier suffix; bare IDs cause errors. if (BARE_PRO_IDS.has(clean)) { @@ -120,7 +123,6 @@ export class AntigravityExecutor extends BaseExecutor { "Content-Type": "application/json", Authorization: `Bearer ${credentials.accessToken}`, "User-Agent": antigravityUserAgent(), - "X-Goog-Api-Client": googApiClientHeader(), Accept: "text/event-stream", "X-OmniRoute-Source": "omniroute", }; @@ -442,7 +444,16 @@ export class AntigravityExecutor extends BaseExecutor { return collect(); } - async execute({ model, body, stream, credentials, signal, log, upstreamExtraHeaders }) { + async execute({ + model, + body, + stream, + credentials, + signal, + log, + upstreamExtraHeaders, + }: ExecuteInput) { + await resolveAntigravityVersion(); const fallbackCount = this.getFallbackCount(); let lastError = null; let lastStatus = 0; @@ -517,7 +528,7 @@ export class AntigravityExecutor extends BaseExecutor { // signal — multi-hour Retry-After upgrades rate_limited to // quota_exhausted so the GOOGLE_ONE_AI credits retry fires). const effectiveRetryHintMs = retryMs ?? parsedRetryMs ?? null; - const category = classify429(errorMessage, effectiveRetryHintMs); + const category = classify429(errorMessage); // 3. For quota_exhausted, attempt Google One AI credits retry FIRST! // Skip if credits were already injected on the first call diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index f6185f8c27..d8cd96ce64 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -132,6 +132,7 @@ import { isClaudeCodeCompatibleProvider, resolveClaudeCodeCompatibleSessionId, } from "../services/claudeCodeCompatible.ts"; +import { setGeminiThoughtSignatureMode } from "../services/geminiThoughtSignatureStore.ts"; function extractMemoryTextFromResponse( response: Record | null | undefined @@ -858,6 +859,7 @@ export async function handleChatCore({ const stream = resolveStreamFlag(body?.stream, acceptHeader); const settings = await getCachedSettings(); + setGeminiThoughtSignatureMode(settings.antigravitySignatureCacheMode); const semanticCacheEnabled = settings.semanticCacheEnabled !== false; // Create request logger for this session: sourceFormat_targetFormat_model diff --git a/open-sse/services/antigravityHeaders.ts b/open-sse/services/antigravityHeaders.ts index 009e9a2d66..c7c0f0c6c9 100644 --- a/open-sse/services/antigravityHeaders.ts +++ b/open-sse/services/antigravityHeaders.ts @@ -1,4 +1,9 @@ import os from "node:os"; +import { + ANTIGRAVITY_FALLBACK_VERSION, + getCachedAntigravityVersion, + resolveAntigravityVersion, +} from "./antigravityVersion.ts"; /** * Antigravity and Gemini CLI header utilities. @@ -11,7 +16,7 @@ import os from "node:os"; type AntigravityHeaderProfile = "loadCodeAssist" | "fetchAvailableModels" | "models"; -const ANTIGRAVITY_VERSION = "1.21.9"; +const ANTIGRAVITY_VERSION = ANTIGRAVITY_FALLBACK_VERSION; const GEMINI_CLI_VERSION = "0.31.0"; const GEMINI_SDK_VERSION = "1.41.0"; const NODE_VERSION = "v22.19.0"; @@ -68,7 +73,12 @@ function getArch(): string { * darwin/arm64. Matches CLIProxyAPI's proven production behavior. */ export function antigravityUserAgent(): string { - return `antigravity/${ANTIGRAVITY_VERSION} darwin/arm64`; + return `antigravity/${getCachedAntigravityVersion()} darwin/arm64`; +} + +export async function resolveAntigravityUserAgent(): Promise { + const version = await resolveAntigravityVersion(); + return `antigravity/${version} darwin/arm64`; } export function getAntigravityLoadCodeAssistMetadata(): Record { diff --git a/open-sse/services/antigravityVersion.ts b/open-sse/services/antigravityVersion.ts new file mode 100644 index 0000000000..e299074941 --- /dev/null +++ b/open-sse/services/antigravityVersion.ts @@ -0,0 +1,141 @@ +const ANTIGRAVITY_RELEASE_FEED_URL = + "https://antigravity-auto-updater-974169037036.us-central1.run.app/releases"; +const ANTIGRAVITY_GITHUB_RELEASE_URL = + "https://api.github.com/repos/antigravityide/antigravity/releases/latest"; + +export const ANTIGRAVITY_VERSION_CACHE_TTL_MS = 6 * 60 * 60 * 1000; +export const ANTIGRAVITY_VERSION_FETCH_TIMEOUT_MS = 5_000; +export const ANTIGRAVITY_FALLBACK_VERSION = "1.23.2"; + +type VersionCache = { + fetchedAt: number; + version: string; +}; + +type FetchLike = typeof fetch; + +let versionCache: VersionCache | null = null; +let inFlightRequest: Promise | null = null; + +function normalizeVersion(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim().replace(/^v/i, ""); + const match = trimmed.match(/^(\d+\.\d+\.\d+)\b/); + return match ? match[1] : null; +} + +async function fetchJsonWithTimeout(fetchImpl: FetchLike, url: string): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), ANTIGRAVITY_VERSION_FETCH_TIMEOUT_MS); + + try { + const response = await fetchImpl(url, { + headers: { + Accept: "application/json", + "User-Agent": "OmniRoute-AntigravityVersion/1.0", + }, + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error(`Version source ${url} returned ${response.status}`); + } + + return response.json(); + } finally { + clearTimeout(timeoutId); + } +} + +function parseOfficialReleaseFeed(payload: unknown): string | null { + if (!Array.isArray(payload)) return null; + + for (const entry of payload) { + const version = normalizeVersion((entry as { version?: unknown })?.version); + if (version) return version; + } + + return null; +} + +function parseGitHubRelease(payload: unknown): string | null { + if (!payload || typeof payload !== "object") return null; + + const candidate = + (payload as { tag_name?: unknown }).tag_name ?? (payload as { name?: unknown }).name; + + return normalizeVersion(candidate); +} + +async function fetchLatestAntigravityVersion(fetchImpl: FetchLike): Promise { + const sources = [ + { + parse: parseOfficialReleaseFeed, + url: ANTIGRAVITY_RELEASE_FEED_URL, + }, + { + parse: parseGitHubRelease, + url: ANTIGRAVITY_GITHUB_RELEASE_URL, + }, + ]; + + for (const source of sources) { + try { + const payload = await fetchJsonWithTimeout(fetchImpl, source.url); + const version = source.parse(payload); + if (version) return version; + } catch { + // Try the next source and fall back to the last known good version if all fail. + } + } + + return null; +} + +export async function resolveAntigravityVersion(fetchImpl: FetchLike = fetch): Promise { + const now = Date.now(); + + if (versionCache && now - versionCache.fetchedAt < ANTIGRAVITY_VERSION_CACHE_TTL_MS) { + return versionCache.version; + } + + if (inFlightRequest) { + return inFlightRequest; + } + + inFlightRequest = (async () => { + const resolved = await fetchLatestAntigravityVersion(fetchImpl); + const version = resolved || versionCache?.version || ANTIGRAVITY_FALLBACK_VERSION; + + if (resolved) { + versionCache = { + fetchedAt: Date.now(), + version, + }; + } + + return version; + })(); + + try { + return await inFlightRequest; + } finally { + inFlightRequest = null; + } +} + +export function getCachedAntigravityVersion(): string { + return versionCache?.version || ANTIGRAVITY_FALLBACK_VERSION; +} + +export function seedAntigravityVersionCache(version: string, fetchedAt = Date.now()): void { + versionCache = { + fetchedAt, + version, + }; +} + +export function clearAntigravityVersionCache(): void { + versionCache = null; + inFlightRequest = null; +} diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index fa076f9de4..065840108a 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -1,6 +1,14 @@ import { createHash, randomUUID } from "node:crypto"; import { getStainlessTimeoutSeconds } from "@/shared/utils/runtimeTimeouts"; +import { + ANTHROPIC_BETA_FULL, + ANTHROPIC_VERSION_HEADER, + CLAUDE_CLI_STAINLESS_PACKAGE_VERSION, + CLAUDE_CLI_STAINLESS_RUNTIME_VERSION, + CLAUDE_CLI_USER_AGENT, + CLAUDE_CLI_VERSION, +} from "../config/anthropicHeaders.ts"; import { prepareClaudeRequest } from "../translator/helpers/claudeHelper.ts"; import { signRequestBody } from "./claudeCodeCCH.ts"; import { computeFingerprint, extractFirstUserMessageText } from "./claudeCodeFingerprint.ts"; @@ -26,11 +34,10 @@ export const CLAUDE_CODE_COMPATIBLE_PREFIX = "anthropic-compatible-cc-"; export const CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH = "/v1/messages?beta=true"; export const CLAUDE_CODE_COMPATIBLE_DEFAULT_MODELS_PATH = "/models"; export const CLAUDE_CODE_COMPATIBLE_DEFAULT_MAX_TOKENS = 8092; -export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_VERSION = "2023-06-01"; -export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA = - "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24,token-efficient-tools-2025-02-19"; -export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.87"; -export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = `claude-cli/${CLAUDE_CODE_COMPATIBLE_VERSION} (external, cli)`; +export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_VERSION = ANTHROPIC_VERSION_HEADER; +export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA = ANTHROPIC_BETA_FULL; +export const CLAUDE_CODE_COMPATIBLE_VERSION = CLAUDE_CLI_VERSION; +export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = CLAUDE_CLI_USER_AGENT; /** * Build the billing header dynamically with fingerprint and CCH placeholder. * The cch=00000 placeholder is later replaced by signRequestBody(). @@ -136,11 +143,11 @@ export function buildClaudeCodeCompatibleHeaders( "X-Stainless-Retry-Count": "0", "X-Stainless-Timeout": String(CLAUDE_CODE_COMPATIBLE_STAINLESS_TIMEOUT_SECONDS), "X-Stainless-Lang": "js", - "X-Stainless-Package-Version": "0.80.0", + "X-Stainless-Package-Version": CLAUDE_CLI_STAINLESS_PACKAGE_VERSION, "X-Stainless-OS": "MacOS", "X-Stainless-Arch": "arm64", "X-Stainless-Runtime": "node", - "X-Stainless-Runtime-Version": "v24.3.0", + "X-Stainless-Runtime-Version": CLAUDE_CLI_STAINLESS_RUNTIME_VERSION, "accept-language": "*", "sec-fetch-mode": "cors", "accept-encoding": "identity", diff --git a/open-sse/services/geminiThoughtSignatureStore.ts b/open-sse/services/geminiThoughtSignatureStore.ts index f52d7a8821..4f47b7e39a 100644 --- a/open-sse/services/geminiThoughtSignatureStore.ts +++ b/open-sse/services/geminiThoughtSignatureStore.ts @@ -1,12 +1,15 @@ const MAX_SIGNATURES = 1000; const TTL_MS = 1000 * 60 * 60; +export type SignatureCacheMode = "enabled" | "bypass" | "bypass-strict"; + type Entry = { signature: string; expiresAt: number; }; const signatures = new Map(); +let signatureCacheMode: SignatureCacheMode = "enabled"; function pruneExpired() { const now = Date.now(); @@ -42,3 +45,120 @@ export function getGeminiThoughtSignature(toolCallId: unknown) { if (!entry) return null; return entry.signature; } + +export function normalizeSignatureCacheMode(value: unknown): SignatureCacheMode { + return value === "bypass" || value === "bypass-strict" ? value : "enabled"; +} + +export function setGeminiThoughtSignatureMode(mode: unknown) { + signatureCacheMode = normalizeSignatureCacheMode(mode); +} + +export function getGeminiThoughtSignatureMode(): SignatureCacheMode { + return signatureCacheMode; +} + +function decodeSignature(signature: string): Buffer | null { + if (!signature || (signature[0] !== "R" && signature[0] !== "E")) return null; + + const payload = signature.slice(1); + if (!/^[A-Za-z0-9+/]+={0,2}$/.test(payload) || payload.length % 4 === 1) { + return null; + } + + try { + const decoded = Buffer.from(payload, "base64"); + if (decoded.length === 0) return null; + + const canonical = decoded.toString("base64").replace(/=+$/g, ""); + if (canonical !== payload.replace(/=+$/g, "")) { + return null; + } + + return decoded; + } catch { + return null; + } +} + +function readVarint( + buffer: Buffer, + startOffset: number +): { nextOffset: number; value: number } | null { + let offset = startOffset; + let result = 0; + let shift = 0; + + while (offset < buffer.length && shift < 35) { + const byte = buffer[offset]; + result |= (byte & 0x7f) << shift; + offset += 1; + + if ((byte & 0x80) === 0) { + return { nextOffset: offset, value: result }; + } + + shift += 7; + } + + return null; +} + +export function isValidBasicGeminiThoughtSignature(signature: unknown): boolean { + if (typeof signature !== "string") return false; + const decoded = decodeSignature(signature); + return Boolean(decoded && decoded[0] === 0x12); +} + +export function isValidFullGeminiThoughtSignature(signature: unknown): boolean { + if (typeof signature !== "string") return false; + const decoded = decodeSignature(signature); + if (!decoded || decoded[0] !== 0x12) return false; + + const outerLength = readVarint(decoded, 1); + if (!outerLength) return false; + + const outerEnd = outerLength.nextOffset + outerLength.value; + if (outerEnd !== decoded.length) return false; + + const inner = decoded.subarray(outerLength.nextOffset, outerEnd); + if (inner.length === 0 || inner[0] !== 0x0a) return false; + + const innerLength = readVarint(inner, 1); + if (!innerLength) return false; + + return innerLength.nextOffset + innerLength.value === inner.length; +} + +export function resolveGeminiThoughtSignature( + toolCallId: unknown, + clientSignature?: unknown +): string | null { + const persisted = getGeminiThoughtSignature(toolCallId); + if (typeof clientSignature !== "string" || clientSignature.length === 0) { + return persisted; + } + + if (signatureCacheMode === "enabled") { + return persisted; + } + + const isValid = + signatureCacheMode === "bypass-strict" + ? isValidFullGeminiThoughtSignature(clientSignature) + : isValidBasicGeminiThoughtSignature(clientSignature); + + if (isValid) { + return clientSignature; + } + + console.warn( + `[signature-cache] ${signatureCacheMode}: invalid client thought signature, falling back` + ); + return persisted; +} + +export function clearGeminiThoughtSignatures() { + signatures.clear(); + signatureCacheMode = "enabled"; +} diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index 138281a3de..e68e06f1dc 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -1,4 +1,5 @@ import { PROVIDER_ID_TO_ALIAS, PROVIDER_MODELS } from "../config/providerModels.ts"; +import { ANTIGRAVITY_MODEL_ALIASES } from "../config/antigravityModelAliases.ts"; import { resolveWildcardAlias } from "./wildcardRouter.ts"; // Derive alias→provider mapping from the single source of truth (PROVIDER_ID_TO_ALIAS) @@ -36,7 +37,7 @@ const PROVIDER_MODEL_ALIASES = { "gpt-oss-120b": "openai/gpt-oss-120b", "nvidia/gpt-oss-120b": "openai/gpt-oss-120b", }, - antigravity: {}, + antigravity: ANTIGRAVITY_MODEL_ALIASES, }; const CROSS_PROXY_MODEL_ALIASES = { diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index 26efbe6d12..f71a578c7a 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -2,7 +2,7 @@ import { register } from "../registry.ts"; import { FORMATS } from "../formats.ts"; import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.ts"; import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/constants.ts"; -import { getGeminiThoughtSignature } from "../../services/geminiThoughtSignatureStore.ts"; +import { resolveGeminiThoughtSignature } from "../../services/geminiThoughtSignatureStore.ts"; import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.ts"; import { capMaxOutputTokens, @@ -98,6 +98,18 @@ function buildChangedToolNameMap(toolNameMap: Map): Map 0 ? new Map(changedEntries) : null; } +function extractClientThoughtSignature(toolCall) { + if (!toolCall || typeof toolCall !== "object") return null; + + return ( + toolCall.thoughtSignature || + toolCall.thought_signature || + toolCall.function?.thoughtSignature || + toolCall.function?.thought_signature || + null + ); +} + // Core: Convert OpenAI request to Gemini format (base for all variants) function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolNameOptions = {}) { const result: GeminiRequest = { @@ -210,7 +222,7 @@ function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolName if (msg.tool_calls && Array.isArray(msg.tool_calls)) { const toolCallIds = []; const firstPersistedSignature = msg.tool_calls - .map((tc) => getGeminiThoughtSignature(tc.id)) + .map((tc) => resolveGeminiThoughtSignature(tc.id, extractClientThoughtSignature(tc))) .find((signature) => typeof signature === "string" && signature.length > 0); const shouldUseEmbeddedSignature = !parts.some((p) => p.thoughtSignature); @@ -219,7 +231,10 @@ function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolName if (tc.type !== "function") continue; const args = tryParseJSON(tc.function?.arguments || "{}"); - const signatureForToolCall = getGeminiThoughtSignature(tc.id); + const signatureForToolCall = resolveGeminiThoughtSignature( + tc.id, + extractClientThoughtSignature(tc) + ); const embeddedThoughtSignature = shouldUseEmbeddedSignature ? firstPersistedSignature || signatureForToolCall || DEFAULT_THINKING_GEMINI_SIGNATURE : undefined; diff --git a/scripts/build-next-isolated.mjs b/scripts/build-next-isolated.mjs index 2cc7194036..52fa1dcaeb 100644 --- a/scripts/build-next-isolated.mjs +++ b/scripts/build-next-isolated.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node import fs from "node:fs/promises"; +import os from "node:os"; import path from "node:path"; import { spawn } from "node:child_process"; import { pathToFileURL } from "node:url"; @@ -13,8 +14,19 @@ import { pathToFileURL } from "node:url"; */ const projectRoot = process.cwd(); -const legacyAppDir = path.join(projectRoot, "app"); -const backupDir = path.join(projectRoot, `.app-build-backup-${process.pid}-${Date.now()}`); +const backupRoot = path.join(os.tmpdir(), `omniroute-build-isolated-${process.pid}-${Date.now()}`); +const transientBuildPaths = [ + { + label: "legacy app snapshot", + sourcePath: path.join(projectRoot, "app"), + backupPath: path.join(backupRoot, "app"), + }, + { + label: "task planning workspace", + sourcePath: path.join(projectRoot, "_tasks"), + backupPath: path.join(backupRoot, "_tasks"), + }, +]; async function exists(targetPath) { try { @@ -26,6 +38,8 @@ async function exists(targetPath) { } export async function movePath(sourcePath, destinationPath, fsImpl = fs) { + await fsImpl.mkdir(path.dirname(destinationPath), { recursive: true }); + try { await fsImpl.rename(sourcePath, destinationPath); } catch (error) { @@ -82,12 +96,13 @@ export function resolveNextBuildEnv(baseEnv = process.env) { } export async function main() { - let moved = false; + const movedPaths = []; try { - if (await exists(legacyAppDir)) { - await movePath(legacyAppDir, backupDir); - moved = true; + for (const entry of transientBuildPaths) { + if (!(await exists(entry.sourcePath))) continue; + await movePath(entry.sourcePath, entry.backupPath); + movedPaths.push(entry); } const result = await runNextBuild(); @@ -113,17 +128,25 @@ export async function main() { console.error("[build-next-isolated] Build failed:", error); process.exitCode = 1; } finally { - if (moved) { + while (movedPaths.length > 0) { + const entry = movedPaths.pop(); + if (!entry) continue; try { - await movePath(backupDir, legacyAppDir); + await movePath(entry.backupPath, entry.sourcePath); } catch (restoreError) { console.error( - `[build-next-isolated] Failed to restore legacy app dir from ${backupDir}:`, + `[build-next-isolated] Failed to restore ${entry.label} from ${entry.backupPath}:`, restoreError ); process.exitCode = 1; } } + + try { + await fs.rm(backupRoot, { recursive: true, force: true }); + } catch (cleanupError) { + console.warn("[build-next-isolated] Failed to clean temporary backup root:", cleanupError); + } } } diff --git a/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx b/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx index f06b7ad2fd..333fa67186 100644 --- a/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx +++ b/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx @@ -311,6 +311,21 @@ function formatFileSize(bytes: number): string { return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } +function fileToDataUrl(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + if (typeof reader.result === "string") { + resolve(reader.result); + return; + } + reject(new Error("Failed to read file")); + }; + reader.onerror = () => reject(reader.error || new Error("Failed to read file")); + reader.readAsDataURL(file); + }); +} + /** Render image result thumbnails */ function ImageResults({ data }: { data: any }) { const images: Array<{ url?: string; b64_json?: string; revised_prompt?: string }> = @@ -383,6 +398,8 @@ export default function MediaPageClient() { const MAX_TRANSCRIPTION_FILE_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB const [audioFile, setAudioFile] = useState(null); const [fileSizeError, setFileSizeError] = useState(null); + const [imageInputFile, setImageInputFile] = useState(null); + const [imageMaskFile, setImageMaskFile] = useState(null); // Fix #390: Track which local providers (sdwebui, comfyui) are actually configured // so we can hide them when they haven't been set up in the providers page @@ -433,6 +450,8 @@ export default function MediaPageClient() { setError(null); setIsCredentialsError(false); setAudioFile(null); + setImageInputFile(null); + setImageMaskFile(null); // Pick first provider and first model automatically const providers = PROVIDER_MODELS[tab] ?? []; const firstProvider = providers[0]; @@ -473,9 +492,10 @@ export default function MediaPageClient() { try { const config = MODALITY_CONFIG[activeTab]; const modelId = selectedModel; + const promptValue = prompt.trim(); if (activeTab === "speech") { - if (!prompt.trim()) { + if (!promptValue) { setError("Please enter text to synthesize."); setLoading(false); return; @@ -485,7 +505,7 @@ export default function MediaPageClient() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: modelId, - input: prompt.trim(), + input: promptValue, voice: speechVoice, response_format: speechFormat, }), @@ -549,19 +569,44 @@ export default function MediaPageClient() { return; } - if (!prompt.trim()) { - setError("Please enter a prompt."); + if (activeTab === "image" && selectedProvider === "topaz" && !imageInputFile) { + setError("Topaz requires an input image."); setLoading(false); return; } + + if (!prompt.trim()) { + if (activeTab !== "image" || selectedProvider !== "topaz") { + setError("Please enter a prompt."); + setLoading(false); + return; + } + } + + const payload: Record = { + model: modelId, + prompt: + promptValue || + (activeTab === "image" && selectedProvider === "topaz" ? "Enhance this image" : ""), + ...(activeTab === "image" ? { size: "1024x1024", n: 1 } : {}), + }; + + if (activeTab === "image" && imageInputFile) { + const imageDataUrl = await fileToDataUrl(imageInputFile); + payload.image_url = imageDataUrl; + payload.imageUrls = [imageDataUrl]; + } + + if (activeTab === "image" && imageMaskFile) { + const maskDataUrl = await fileToDataUrl(imageMaskFile); + payload.mask = maskDataUrl; + payload.mask_url = maskDataUrl; + } + const res = await fetch(config.endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: modelId, - prompt: prompt.trim(), - ...(activeTab === "image" ? { size: "1024x1024", n: 1 } : {}), - }), + body: JSON.stringify(payload), }); if (!res.ok) { const raw = await res.json().catch(() => ({})); @@ -579,6 +624,16 @@ export default function MediaPageClient() { const config = MODALITY_CONFIG[activeTab]; const voiceList = getVoiceList(selectedProvider); + const isTopazImageFlow = activeTab === "image" && selectedProvider === "topaz"; + const isGenerateDisabled = + loading || + (activeTab === "transcription" + ? !audioFile + : activeTab === "image" + ? isTopazImageFlow + ? !imageInputFile + : !prompt.trim() + : !prompt.trim()); return (
@@ -735,27 +790,80 @@ export default function MediaPageClient() {

) : ( - /* Prompt / Text */ -
- -