refactor(open-sse): remove dead buildProviderUrl/Headers path (A1)
These translate-path builders had no runtime consumers: the translator route uses executor.buildUrl/buildHeaders, and the barrel re-exports were unused. Removing them eliminates the parallel URL/header build path (single source of truth = executors). Drop their private helpers and the now-unused clineAuth import. Golden executor snapshots + gate: no regression. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,8 +31,6 @@ export {
|
||||
export {
|
||||
detectFormat,
|
||||
getProviderConfig,
|
||||
buildProviderUrl,
|
||||
buildProviderHeaders,
|
||||
getTargetFormat
|
||||
} from "./services/provider.js";
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PROVIDERS } from "../config/providers.js";
|
||||
import { buildClineHeaders } from "../shared/clineAuth.js";
|
||||
|
||||
const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
|
||||
const OPENAI_COMPATIBLE_DEFAULTS = {
|
||||
@@ -24,27 +23,6 @@ function getOpenAICompatibleType(provider) {
|
||||
return provider.includes("responses") ? "responses" : "chat";
|
||||
}
|
||||
|
||||
function buildOpenAICompatibleUrl(baseUrl, apiType) {
|
||||
const normalized = baseUrl.replace(/\/$/, "");
|
||||
const path = apiType === "responses" ? "/responses" : "/chat/completions";
|
||||
return `${normalized}${path}`;
|
||||
}
|
||||
|
||||
function buildAnthropicCompatibleUrl(baseUrl) {
|
||||
const normalized = baseUrl.replace(/\/$/, "");
|
||||
return `${normalized}/messages`;
|
||||
}
|
||||
|
||||
function buildQwenBaseUrl(resourceUrl, fallbackBaseUrl) {
|
||||
const fallback = (fallbackBaseUrl || "").replace(/\/chat\/completions$/, "");
|
||||
const raw = typeof resourceUrl === "string" ? resourceUrl.trim() : "";
|
||||
if (!raw) return fallback;
|
||||
if (raw.startsWith("http://") || raw.startsWith("https://")) {
|
||||
return raw.replace(/\/$/, "");
|
||||
}
|
||||
return `https://${raw.replace(/\/$/, "")}/v1`;
|
||||
}
|
||||
|
||||
// Detect request format from body structure
|
||||
export function detectFormat(body) {
|
||||
// OpenAI Responses API: has input (array or string) instead of messages[]
|
||||
@@ -145,180 +123,6 @@ export function getProviderConfig(provider) {
|
||||
return PROVIDERS[provider] || PROVIDERS.openai;
|
||||
}
|
||||
|
||||
// Get number of fallback URLs for provider (for retry logic)
|
||||
export function getProviderFallbackCount(provider) {
|
||||
const config = getProviderConfig(provider);
|
||||
return config.baseUrls?.length || 1;
|
||||
}
|
||||
|
||||
// Build provider URL
|
||||
export function buildProviderUrl(provider, model, stream = true, options = {}) {
|
||||
if (isOpenAICompatible(provider)) {
|
||||
const apiType = getOpenAICompatibleType(provider);
|
||||
const baseUrl = options?.baseUrl || OPENAI_COMPATIBLE_DEFAULTS.baseUrl;
|
||||
return buildOpenAICompatibleUrl(baseUrl, apiType);
|
||||
}
|
||||
if (isAnthropicCompatible(provider)) {
|
||||
const baseUrl = options?.baseUrl || ANTHROPIC_COMPATIBLE_DEFAULTS.baseUrl;
|
||||
return buildAnthropicCompatibleUrl(baseUrl);
|
||||
}
|
||||
const config = getProviderConfig(provider);
|
||||
|
||||
switch (provider) {
|
||||
case "claude":
|
||||
return `${config.baseUrl}?beta=true`;
|
||||
|
||||
case "gemini": {
|
||||
const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
|
||||
return `${config.baseUrl}/${model}:${action}`;
|
||||
}
|
||||
|
||||
case "gemini-cli": {
|
||||
const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
|
||||
return `${config.baseUrl}:${action}`;
|
||||
}
|
||||
|
||||
case "antigravity": {
|
||||
// Use baseUrlIndex from options or default to 0
|
||||
const urlIndex = options?.baseUrlIndex || 0;
|
||||
const baseUrl = config.baseUrls[urlIndex] || config.baseUrls[0];
|
||||
const path = stream ? "/v1internal:streamGenerateContent?alt=sse" : "/v1internal:generateContent";
|
||||
return `${baseUrl}${path}`;
|
||||
}
|
||||
|
||||
case "codex":
|
||||
return config.baseUrl;
|
||||
|
||||
case "qwen": {
|
||||
const baseUrl = buildQwenBaseUrl(options?.qwenResourceUrl, config.baseUrl);
|
||||
return `${baseUrl}/chat/completions`;
|
||||
}
|
||||
|
||||
case "github":
|
||||
return config.baseUrl;
|
||||
|
||||
case "glm":
|
||||
case "kimi":
|
||||
case "minimax":
|
||||
// Claude-compatible providers
|
||||
return `${config.baseUrl}?beta=true`;
|
||||
|
||||
default:
|
||||
return config.baseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Build provider headers
|
||||
export function buildProviderHeaders(provider, credentials, stream = true, body = null) {
|
||||
const config = getProviderConfig(provider);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...config.headers
|
||||
};
|
||||
|
||||
// Add auth header
|
||||
// Specific override for Anthropic Compatible
|
||||
if (isAnthropicCompatible(provider)) {
|
||||
if (credentials.apiKey) {
|
||||
headers["x-api-key"] = credentials.apiKey;
|
||||
// Do NOT send Authorization header when apiKey is present for Anthropic Compatible
|
||||
// as it causes issues with some providers (e.g. opencode.ai)
|
||||
} else if (credentials.accessToken) {
|
||||
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
||||
}
|
||||
// Add default Anthropic version if not present (some proxies require it)
|
||||
if (!headers["anthropic-version"]) {
|
||||
headers["anthropic-version"] = "2023-06-01";
|
||||
}
|
||||
} else {
|
||||
switch (provider) {
|
||||
case "gemini":
|
||||
if (credentials.apiKey) {
|
||||
headers["x-goog-api-key"] = credentials.apiKey;
|
||||
} else if (credentials.accessToken) {
|
||||
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case "antigravity":
|
||||
case "gemini-cli":
|
||||
// Antigravity and Gemini CLI use OAuth access token
|
||||
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
||||
break;
|
||||
|
||||
case "claude":
|
||||
// Claude uses x-api-key header for API key, or Authorization for OAuth
|
||||
if (credentials.apiKey) {
|
||||
headers["x-api-key"] = credentials.apiKey;
|
||||
} else if (credentials.accessToken) {
|
||||
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case "github": {
|
||||
// GitHub Copilot requires special headers to mimic VSCode
|
||||
// Prioritize copilotToken from providerSpecificData, fallback to accessToken
|
||||
const githubToken = credentials.copilotToken || credentials.accessToken;
|
||||
// Add headers in exact same order as test endpoint
|
||||
headers["Authorization"] = `Bearer ${githubToken}`;
|
||||
headers["Content-Type"] = "application/json";
|
||||
headers["copilot-integration-id"] = "vscode-chat";
|
||||
headers["editor-version"] = "vscode/1.107.1";
|
||||
headers["editor-plugin-version"] = "copilot-chat/0.26.7";
|
||||
headers["user-agent"] = "GitHubCopilotChat/0.26.7";
|
||||
headers["openai-intent"] = "conversation-panel";
|
||||
headers["x-github-api-version"] = "2025-04-01";
|
||||
// Generate a UUID for x-request-id (Cloudflare Workers compatible)
|
||||
headers["x-request-id"] = crypto.randomUUID ? crypto.randomUUID() :
|
||||
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
headers["x-vscode-user-agent-library-version"] = "electron-fetch";
|
||||
headers["X-Initiator"] = "user";
|
||||
headers["Accept"] = "application/json";
|
||||
break;
|
||||
}
|
||||
|
||||
case "codex":
|
||||
case "qwen":
|
||||
case "openai":
|
||||
case "openrouter":
|
||||
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
|
||||
break;
|
||||
|
||||
case "cline":
|
||||
Object.assign(headers, buildClineHeaders(credentials.apiKey || credentials.accessToken));
|
||||
break;
|
||||
|
||||
case "glm":
|
||||
case "kimi":
|
||||
case "minimax":
|
||||
// Claude-compatible API providers use x-api-key
|
||||
headers["x-api-key"] = credentials.apiKey;
|
||||
break;
|
||||
|
||||
case "vertex":
|
||||
case "vertex-partner":
|
||||
// Vertex uses async token minting — headers are set by VertexExecutor._buildHeadersAsync()
|
||||
// Do NOT set Authorization here; it would leak the raw SA JSON as Bearer token
|
||||
break;
|
||||
|
||||
default:
|
||||
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Stream accept header
|
||||
if (stream) {
|
||||
headers["Accept"] = "text/event-stream";
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Get target format for provider
|
||||
export function getTargetFormat(provider) {
|
||||
if (isOpenAICompatible(provider)) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,68 +0,0 @@
|
||||
// A1 GOLDEN: lock OUTPUT của services/provider.js (translate route path) trên code CŨ.
|
||||
// buildProviderUrl/buildProviderHeaders/getTargetFormat — đường SONG SONG với executor.
|
||||
// Mục tiêu: trước khi hợp nhất 2 đường, chốt behavior translate-path hiện tại.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { PROVIDERS } from "../../open-sse/config/providers.js";
|
||||
import {
|
||||
buildProviderUrl,
|
||||
buildProviderHeaders,
|
||||
getTargetFormat,
|
||||
} from "../../open-sse/services/provider.js";
|
||||
|
||||
const API_KEY_CRED = { apiKey: "sk-test-APIKEY", providerSpecificData: {} };
|
||||
const OAUTH_CRED = { accessToken: "tok-test-ACCESS", providerSpecificData: {} };
|
||||
|
||||
// Khử token + field động (github x-request-id uuid, kimi device-id) để snapshot ổn định.
|
||||
function sanitize(headers) {
|
||||
const out = {};
|
||||
for (const [k, v] of Object.entries(headers)) {
|
||||
out[k] = typeof v === "string"
|
||||
? v.replace(/Bearer .+/, "Bearer <TOK>")
|
||||
.replace(/sk-test-APIKEY|tok-test-ACCESS/g, "<CRED>")
|
||||
.replace(/kimi-\d{10,}/g, "kimi-<TS>")
|
||||
.replace(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "<UUID>")
|
||||
: v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const providerIds = Object.keys(PROVIDERS).sort();
|
||||
|
||||
function safe(fn) {
|
||||
try { return fn(); } catch (e) { return `THROW: ${e.message}`; }
|
||||
}
|
||||
|
||||
describe("GOLDEN provider.js buildProviderUrl (translate path)", () => {
|
||||
for (const pid of providerIds) {
|
||||
it(`${pid} → url`, () => {
|
||||
const snap = {
|
||||
stream: safe(() => buildProviderUrl(pid, "test-model", true, {})),
|
||||
nonStream: safe(() => buildProviderUrl(pid, "test-model", false, {})),
|
||||
};
|
||||
expect(snap).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("GOLDEN provider.js buildProviderHeaders (translate path)", () => {
|
||||
for (const pid of providerIds) {
|
||||
it(`${pid} → headers`, () => {
|
||||
const cred = PROVIDERS[pid].noAuth ? {} : API_KEY_CRED;
|
||||
const credOauth = PROVIDERS[pid].noAuth ? {} : OAUTH_CRED;
|
||||
const snap = {
|
||||
apiKey: safe(() => sanitize(buildProviderHeaders(pid, cred, true))),
|
||||
oauth: safe(() => sanitize(buildProviderHeaders(pid, credOauth, true))),
|
||||
nonStream: safe(() => sanitize(buildProviderHeaders(pid, cred, false))),
|
||||
};
|
||||
expect(snap).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("GOLDEN provider.js getTargetFormat", () => {
|
||||
it("all providers → format", () => {
|
||||
const snap = {};
|
||||
for (const pid of providerIds) snap[pid] = safe(() => getTargetFormat(pid));
|
||||
expect(snap).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user