refactor: move platform discovery out of model service
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
function readSource(relativePath: string): string {
|
||||||
|
return readFileSync(new URL(relativePath, import.meta.url), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modelService discovery architecture boundaries', () => {
|
||||||
|
it('keeps platform-specific oauth discovery HTTP logic in a dedicated discovery registry', () => {
|
||||||
|
const source = readSource('./modelService.ts');
|
||||||
|
|
||||||
|
expect(source).toContain("from './platformDiscoveryRegistry.js'");
|
||||||
|
expect(source).not.toContain('function discoverCodexModelsFromCloud');
|
||||||
|
expect(source).not.toContain('function discoverClaudeModelsFromCloud');
|
||||||
|
expect(source).not.toContain('function validateGeminiCliOauthConnection');
|
||||||
|
expect(source).not.toContain('function discoverAntigravityModelsFromCloud');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { and, eq } from 'drizzle-orm';
|
import { and, eq } from 'drizzle-orm';
|
||||||
import { fetch } from 'undici';
|
|
||||||
import { db, schema } from '../db/index.js';
|
import { db, schema } from '../db/index.js';
|
||||||
import { getAdapter } from './platforms/index.js';
|
import { getAdapter } from './platforms/index.js';
|
||||||
import {
|
import {
|
||||||
@@ -20,27 +19,20 @@ import {
|
|||||||
import { invalidateTokenRouterCache } from './tokenRouter.js';
|
import { invalidateTokenRouterCache } from './tokenRouter.js';
|
||||||
import { setAccountRuntimeHealth } from './accountHealthService.js';
|
import { setAccountRuntimeHealth } from './accountHealthService.js';
|
||||||
import { clearAllRouteDecisionSnapshots } from './routeDecisionSnapshotStore.js';
|
import { clearAllRouteDecisionSnapshots } from './routeDecisionSnapshotStore.js';
|
||||||
import { withAccountProxyOverride, withExplicitProxyRequestInit, withSiteRecordProxyRequestInit } from './siteProxy.js';
|
import { withAccountProxyOverride } from './siteProxy.js';
|
||||||
import { isCodexPlatform } from './oauth/codexAccount.js';
|
import { isCodexPlatform } from './oauth/codexAccount.js';
|
||||||
import { buildStoredOauthStateFromAccount, getOauthInfoFromAccount } from './oauth/oauthAccount.js';
|
import { buildStoredOauthStateFromAccount, getOauthInfoFromAccount } from './oauth/oauthAccount.js';
|
||||||
import { CLAUDE_DEFAULT_ANTHROPIC_VERSION } from './oauth/claudeProvider.js';
|
|
||||||
import { refreshOauthAccessTokenSingleflight } from './oauth/refreshSingleflight.js';
|
import { refreshOauthAccessTokenSingleflight } from './oauth/refreshSingleflight.js';
|
||||||
import {
|
import {
|
||||||
ANTIGRAVITY_DAILY_UPSTREAM_BASE_URL,
|
discoverAntigravityModelsFromCloud,
|
||||||
ANTIGRAVITY_MODELS_USER_AGENT,
|
discoverClaudeModelsFromCloud,
|
||||||
ANTIGRAVITY_SANDBOX_DAILY_UPSTREAM_BASE_URL,
|
discoverCodexModelsFromCloud,
|
||||||
ANTIGRAVITY_UPSTREAM_BASE_URL,
|
validateGeminiCliOauthConnection,
|
||||||
} from './oauth/antigravityProvider.js';
|
} from './platformDiscoveryRegistry.js';
|
||||||
import {
|
|
||||||
GEMINI_CLI_GOOGLE_API_CLIENT,
|
|
||||||
GEMINI_CLI_REQUIRED_SERVICE,
|
|
||||||
GEMINI_CLI_USER_AGENT,
|
|
||||||
} from './oauth/geminiCliProvider.js';
|
|
||||||
|
|
||||||
const API_TOKEN_DISCOVERY_TIMEOUT_MS = 8_000;
|
const API_TOKEN_DISCOVERY_TIMEOUT_MS = 8_000;
|
||||||
const MODEL_DISCOVERY_TIMEOUT_MS = 12_000;
|
const MODEL_DISCOVERY_TIMEOUT_MS = 12_000;
|
||||||
const MODEL_REFRESH_BATCH_SIZE = 3;
|
const MODEL_REFRESH_BATCH_SIZE = 3;
|
||||||
const CODEX_MODELS_CLIENT_VERSION = '1.0.0';
|
|
||||||
const GEMINI_CLI_STATIC_MODELS = [
|
const GEMINI_CLI_STATIC_MODELS = [
|
||||||
'gemini-2.5-pro',
|
'gemini-2.5-pro',
|
||||||
'gemini-2.5-flash',
|
'gemini-2.5-flash',
|
||||||
@@ -152,88 +144,6 @@ function normalizeModels(models: string[]): string[] {
|
|||||||
return normalizedModels;
|
return normalizedModels;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCodexModelsEndpoint(baseUrl: string): string {
|
|
||||||
const normalized = (baseUrl || '').replace(/\/+$/, '');
|
|
||||||
return `${normalized}/models?client_version=${encodeURIComponent(CODEX_MODELS_CLIENT_VERSION)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractCodexModelIds(payload: unknown): string[] {
|
|
||||||
const collection = (() => {
|
|
||||||
if (Array.isArray(payload)) return payload;
|
|
||||||
if (!payload || typeof payload !== 'object') return [];
|
|
||||||
const record = payload as Record<string, unknown>;
|
|
||||||
if (Array.isArray(record.models)) return record.models;
|
|
||||||
if (Array.isArray(record.data)) return record.data;
|
|
||||||
if (Array.isArray(record.items)) return record.items;
|
|
||||||
return [];
|
|
||||||
})();
|
|
||||||
|
|
||||||
return collection.flatMap((item) => {
|
|
||||||
if (typeof item === 'string') return [item];
|
|
||||||
if (!item || typeof item !== 'object' || Array.isArray(item)) return [];
|
|
||||||
const record = item as Record<string, unknown>;
|
|
||||||
const id = typeof record.id === 'string'
|
|
||||||
? record.id
|
|
||||||
: (typeof record.slug === 'string' ? record.slug : (typeof record.model === 'string' ? record.model : ''));
|
|
||||||
return id ? [id] : [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractClaudeModelIds(payload: unknown): string[] {
|
|
||||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return [];
|
|
||||||
const record = payload as { data?: unknown };
|
|
||||||
if (!Array.isArray(record.data)) return [];
|
|
||||||
|
|
||||||
return record.data.flatMap((item) => {
|
|
||||||
if (!item || typeof item !== 'object' || Array.isArray(item)) return [];
|
|
||||||
const value = item as { id?: unknown; name?: unknown };
|
|
||||||
const id = typeof value.id === 'string'
|
|
||||||
? value.id.trim()
|
|
||||||
: (typeof value.name === 'string' ? value.name.trim() : '');
|
|
||||||
return id ? [id] : [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeBaseUrl(baseUrl: string): string {
|
|
||||||
return (baseUrl || '').replace(/\/+$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractAntigravityModelIds(payload: unknown): string[] {
|
|
||||||
if (!payload || typeof payload !== 'object') return [];
|
|
||||||
const record = payload as { models?: unknown };
|
|
||||||
if (record.models && typeof record.models === 'object' && !Array.isArray(record.models)) {
|
|
||||||
return Object.keys(record.models).map((name) => name.trim()).filter(Boolean);
|
|
||||||
}
|
|
||||||
if (!Array.isArray(record.models)) return [];
|
|
||||||
return record.models.flatMap((item) => {
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
const trimmed = item.trim();
|
|
||||||
return trimmed ? [trimmed] : [];
|
|
||||||
}
|
|
||||||
if (!item || typeof item !== 'object' || Array.isArray(item)) return [];
|
|
||||||
const value = item as { id?: unknown; name?: unknown };
|
|
||||||
const id = typeof value.id === 'string'
|
|
||||||
? value.id.trim()
|
|
||||||
: (typeof value.name === 'string' ? value.name.trim() : '');
|
|
||||||
return id ? [id] : [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAntigravityDiscoveryBaseUrls(siteUrl: string): string[] {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
return [
|
|
||||||
siteUrl,
|
|
||||||
ANTIGRAVITY_UPSTREAM_BASE_URL,
|
|
||||||
ANTIGRAVITY_DAILY_UPSTREAM_BASE_URL,
|
|
||||||
ANTIGRAVITY_SANDBOX_DAILY_UPSTREAM_BASE_URL,
|
|
||||||
].flatMap((candidate) => {
|
|
||||||
const normalized = normalizeBaseUrl(candidate);
|
|
||||||
if (!normalized || seen.has(normalized)) return [];
|
|
||||||
seen.add(normalized);
|
|
||||||
return [normalized];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateOauthModelDiscoveryState(input: {
|
async function updateOauthModelDiscoveryState(input: {
|
||||||
account: typeof schema.accounts.$inferSelect;
|
account: typeof schema.accounts.$inferSelect;
|
||||||
checkedAt: string;
|
checkedAt: string;
|
||||||
@@ -259,138 +169,6 @@ async function updateOauthModelDiscoveryState(input: {
|
|||||||
return extraConfig;
|
return extraConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function discoverCodexModelsFromCloud(input: {
|
|
||||||
site: typeof schema.sites.$inferSelect;
|
|
||||||
account: typeof schema.accounts.$inferSelect;
|
|
||||||
}): Promise<string[]> {
|
|
||||||
const accessToken = (input.account.accessToken || '').trim();
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('codex oauth access token missing');
|
|
||||||
}
|
|
||||||
const oauth = getOauthInfoFromAccount(input.account);
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
Accept: 'application/json',
|
|
||||||
Originator: 'codex_cli_rs',
|
|
||||||
};
|
|
||||||
if (oauth?.accountId) {
|
|
||||||
headers['Chatgpt-Account-Id'] = oauth.accountId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
buildCodexModelsEndpoint(input.site.url),
|
|
||||||
withSiteRecordProxyRequestInit(input.site, { method: 'GET', headers }),
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text().catch(() => '');
|
|
||||||
throw new Error(`HTTP ${response.status}: ${text || 'codex model discovery failed'}`);
|
|
||||||
}
|
|
||||||
return normalizeModels(extractCodexModelIds(await response.json()));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function discoverClaudeModelsFromCloud(input: {
|
|
||||||
site: typeof schema.sites.$inferSelect;
|
|
||||||
account: typeof schema.accounts.$inferSelect;
|
|
||||||
}): Promise<string[]> {
|
|
||||||
const accessToken = (input.account.accessToken || '').trim();
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('claude oauth access token missing');
|
|
||||||
}
|
|
||||||
const response = await fetch(
|
|
||||||
`${input.site.url.replace(/\/+$/, '')}/v1/models`,
|
|
||||||
withSiteRecordProxyRequestInit(input.site, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
Accept: 'application/json',
|
|
||||||
'anthropic-version': CLAUDE_DEFAULT_ANTHROPIC_VERSION,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text().catch(() => '');
|
|
||||||
throw new Error(`HTTP ${response.status}: ${text || 'claude oauth model discovery failed'}`);
|
|
||||||
}
|
|
||||||
return normalizeModels(extractClaudeModelIds(await response.json()));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateGeminiCliOauthConnection(input: {
|
|
||||||
account: typeof schema.accounts.$inferSelect;
|
|
||||||
}): Promise<void> {
|
|
||||||
const accessToken = (input.account.accessToken || '').trim();
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('gemini cli oauth access token missing');
|
|
||||||
}
|
|
||||||
const oauth = getOauthInfoFromAccount(input.account);
|
|
||||||
const projectId = (oauth?.projectId || '').trim();
|
|
||||||
if (!projectId) {
|
|
||||||
throw new Error('gemini cli oauth project id missing');
|
|
||||||
}
|
|
||||||
const response = await fetch(
|
|
||||||
`https://serviceusage.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(GEMINI_CLI_REQUIRED_SERVICE)}`,
|
|
||||||
withExplicitProxyRequestInit(getProxyUrlFromExtraConfig(input.account.extraConfig), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
Accept: 'application/json',
|
|
||||||
'User-Agent': GEMINI_CLI_USER_AGENT,
|
|
||||||
'X-Goog-Api-Client': GEMINI_CLI_GOOGLE_API_CLIENT,
|
|
||||||
},
|
|
||||||
}, true),
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text().catch(() => '');
|
|
||||||
throw new Error(`HTTP ${response.status}: ${text || 'gemini cli oauth validation failed'}`);
|
|
||||||
}
|
|
||||||
const payload = await response.json() as { state?: unknown };
|
|
||||||
if (String(payload.state || '').trim().toUpperCase() !== 'ENABLED') {
|
|
||||||
throw new Error(`Cloud AI API not enabled for project ${projectId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function discoverAntigravityModelsFromCloud(input: {
|
|
||||||
site: typeof schema.sites.$inferSelect;
|
|
||||||
account: typeof schema.accounts.$inferSelect;
|
|
||||||
}): Promise<string[]> {
|
|
||||||
const accessToken = (input.account.accessToken || '').trim();
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('antigravity oauth access token missing');
|
|
||||||
}
|
|
||||||
|
|
||||||
const oauth = getOauthInfoFromAccount(input.account);
|
|
||||||
const requestBody = oauth?.projectId ? { project: oauth.projectId } : {};
|
|
||||||
let lastError = '';
|
|
||||||
|
|
||||||
for (const baseUrl of buildAntigravityDiscoveryBaseUrls(input.site.url || ANTIGRAVITY_UPSTREAM_BASE_URL)) {
|
|
||||||
const response = await fetch(
|
|
||||||
`${baseUrl}/v1internal:fetchAvailableModels`,
|
|
||||||
withSiteRecordProxyRequestInit(input.site, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'User-Agent': ANTIGRAVITY_MODELS_USER_AGENT,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestBody),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
|
||||||
lastError = await response.text().catch(() => '') || `HTTP ${response.status}`;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await response.json();
|
|
||||||
const models = normalizeModels(extractAntigravityModelIds(payload));
|
|
||||||
if (models.length > 0) {
|
|
||||||
return models;
|
|
||||||
}
|
|
||||||
lastError = '未获取到可用模型';
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(lastError || '未获取到可用模型');
|
|
||||||
}
|
|
||||||
|
|
||||||
function isExactModelPattern(modelPattern: string): boolean {
|
function isExactModelPattern(modelPattern: string): boolean {
|
||||||
const normalized = modelPattern.trim();
|
const normalized = modelPattern.trim();
|
||||||
if (!normalized) return false;
|
if (!normalized) return false;
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import { fetch } from 'undici';
|
||||||
|
import { schema } from '../db/index.js';
|
||||||
|
import { getProxyUrlFromExtraConfig } from './accountExtraConfig.js';
|
||||||
|
import { withExplicitProxyRequestInit, withSiteRecordProxyRequestInit } from './siteProxy.js';
|
||||||
|
import { getOauthInfoFromAccount } from './oauth/oauthAccount.js';
|
||||||
|
import { CLAUDE_DEFAULT_ANTHROPIC_VERSION } from './oauth/claudeProvider.js';
|
||||||
|
import {
|
||||||
|
ANTIGRAVITY_DAILY_UPSTREAM_BASE_URL,
|
||||||
|
ANTIGRAVITY_MODELS_USER_AGENT,
|
||||||
|
ANTIGRAVITY_SANDBOX_DAILY_UPSTREAM_BASE_URL,
|
||||||
|
ANTIGRAVITY_UPSTREAM_BASE_URL,
|
||||||
|
} from './oauth/antigravityProvider.js';
|
||||||
|
import {
|
||||||
|
GEMINI_CLI_GOOGLE_API_CLIENT,
|
||||||
|
GEMINI_CLI_REQUIRED_SERVICE,
|
||||||
|
GEMINI_CLI_USER_AGENT,
|
||||||
|
} from './oauth/geminiCliProvider.js';
|
||||||
|
|
||||||
|
type PlatformDiscoverySite = typeof schema.sites.$inferSelect;
|
||||||
|
type PlatformDiscoveryAccount = typeof schema.accounts.$inferSelect;
|
||||||
|
|
||||||
|
function normalizeDiscoveredModels(models: string[]): string[] {
|
||||||
|
const normalizedModels: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
for (const rawModel of models) {
|
||||||
|
if (typeof rawModel !== 'string') continue;
|
||||||
|
const modelName = rawModel.trim();
|
||||||
|
if (!modelName) continue;
|
||||||
|
|
||||||
|
const dedupeKey = modelName.toLowerCase();
|
||||||
|
if (seen.has(dedupeKey)) continue;
|
||||||
|
seen.add(dedupeKey);
|
||||||
|
normalizedModels.push(modelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedModels;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBaseUrl(baseUrl: string): string {
|
||||||
|
return (baseUrl || '').replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCodexModelsEndpoint(baseUrl: string): string {
|
||||||
|
const normalized = normalizeBaseUrl(baseUrl);
|
||||||
|
return `${normalized}/models?client_version=${encodeURIComponent('1.0.0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractCodexModelIds(payload: unknown): string[] {
|
||||||
|
const collection = (() => {
|
||||||
|
if (Array.isArray(payload)) return payload;
|
||||||
|
if (!payload || typeof payload !== 'object') return [];
|
||||||
|
const record = payload as Record<string, unknown>;
|
||||||
|
if (Array.isArray(record.models)) return record.models;
|
||||||
|
if (Array.isArray(record.data)) return record.data;
|
||||||
|
if (Array.isArray(record.items)) return record.items;
|
||||||
|
return [];
|
||||||
|
})();
|
||||||
|
|
||||||
|
return collection.flatMap((item) => {
|
||||||
|
if (typeof item === 'string') return [item];
|
||||||
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return [];
|
||||||
|
const record = item as Record<string, unknown>;
|
||||||
|
const id = typeof record.id === 'string'
|
||||||
|
? record.id
|
||||||
|
: (typeof record.slug === 'string' ? record.slug : (typeof record.model === 'string' ? record.model : ''));
|
||||||
|
return id ? [id] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractClaudeModelIds(payload: unknown): string[] {
|
||||||
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return [];
|
||||||
|
const record = payload as { data?: unknown };
|
||||||
|
if (!Array.isArray(record.data)) return [];
|
||||||
|
|
||||||
|
return record.data.flatMap((item) => {
|
||||||
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return [];
|
||||||
|
const value = item as { id?: unknown; name?: unknown };
|
||||||
|
const id = typeof value.id === 'string'
|
||||||
|
? value.id.trim()
|
||||||
|
: (typeof value.name === 'string' ? value.name.trim() : '');
|
||||||
|
return id ? [id] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAntigravityModelIds(payload: unknown): string[] {
|
||||||
|
if (!payload || typeof payload !== 'object') return [];
|
||||||
|
const record = payload as { models?: unknown };
|
||||||
|
if (record.models && typeof record.models === 'object' && !Array.isArray(record.models)) {
|
||||||
|
return Object.keys(record.models).map((name) => name.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(record.models)) return [];
|
||||||
|
return record.models.flatMap((item) => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
const trimmed = item.trim();
|
||||||
|
return trimmed ? [trimmed] : [];
|
||||||
|
}
|
||||||
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return [];
|
||||||
|
const value = item as { id?: unknown; name?: unknown };
|
||||||
|
const id = typeof value.id === 'string'
|
||||||
|
? value.id.trim()
|
||||||
|
: (typeof value.name === 'string' ? value.name.trim() : '');
|
||||||
|
return id ? [id] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAntigravityDiscoveryBaseUrls(siteUrl: string): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return [
|
||||||
|
siteUrl,
|
||||||
|
ANTIGRAVITY_UPSTREAM_BASE_URL,
|
||||||
|
ANTIGRAVITY_DAILY_UPSTREAM_BASE_URL,
|
||||||
|
ANTIGRAVITY_SANDBOX_DAILY_UPSTREAM_BASE_URL,
|
||||||
|
].flatMap((candidate) => {
|
||||||
|
const normalized = normalizeBaseUrl(candidate);
|
||||||
|
if (!normalized || seen.has(normalized)) return [];
|
||||||
|
seen.add(normalized);
|
||||||
|
return [normalized];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function discoverCodexModelsFromCloud(input: {
|
||||||
|
site: PlatformDiscoverySite;
|
||||||
|
account: PlatformDiscoveryAccount;
|
||||||
|
}): Promise<string[]> {
|
||||||
|
const accessToken = (input.account.accessToken || '').trim();
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('codex oauth access token missing');
|
||||||
|
}
|
||||||
|
const oauth = getOauthInfoFromAccount(input.account);
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: 'application/json',
|
||||||
|
Originator: 'codex_cli_rs',
|
||||||
|
};
|
||||||
|
if (oauth?.accountId) {
|
||||||
|
headers['Chatgpt-Account-Id'] = oauth.accountId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
buildCodexModelsEndpoint(input.site.url),
|
||||||
|
withSiteRecordProxyRequestInit(input.site, { method: 'GET', headers }),
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text().catch(() => '');
|
||||||
|
throw new Error(`HTTP ${response.status}: ${text || 'codex model discovery failed'}`);
|
||||||
|
}
|
||||||
|
return normalizeDiscoveredModels(extractCodexModelIds(await response.json()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function discoverClaudeModelsFromCloud(input: {
|
||||||
|
site: PlatformDiscoverySite;
|
||||||
|
account: PlatformDiscoveryAccount;
|
||||||
|
}): Promise<string[]> {
|
||||||
|
const accessToken = (input.account.accessToken || '').trim();
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('claude oauth access token missing');
|
||||||
|
}
|
||||||
|
const response = await fetch(
|
||||||
|
`${input.site.url.replace(/\/+$/, '')}/v1/models`,
|
||||||
|
withSiteRecordProxyRequestInit(input.site, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: 'application/json',
|
||||||
|
'anthropic-version': CLAUDE_DEFAULT_ANTHROPIC_VERSION,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text().catch(() => '');
|
||||||
|
throw new Error(`HTTP ${response.status}: ${text || 'claude oauth model discovery failed'}`);
|
||||||
|
}
|
||||||
|
return normalizeDiscoveredModels(extractClaudeModelIds(await response.json()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateGeminiCliOauthConnection(input: {
|
||||||
|
account: PlatformDiscoveryAccount;
|
||||||
|
}): Promise<void> {
|
||||||
|
const accessToken = (input.account.accessToken || '').trim();
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('gemini cli oauth access token missing');
|
||||||
|
}
|
||||||
|
const oauth = getOauthInfoFromAccount(input.account);
|
||||||
|
const projectId = (oauth?.projectId || '').trim();
|
||||||
|
if (!projectId) {
|
||||||
|
throw new Error('gemini cli oauth project id missing');
|
||||||
|
}
|
||||||
|
const response = await fetch(
|
||||||
|
`https://serviceusage.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(GEMINI_CLI_REQUIRED_SERVICE)}`,
|
||||||
|
withExplicitProxyRequestInit(getProxyUrlFromExtraConfig(input.account.extraConfig), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: 'application/json',
|
||||||
|
'User-Agent': GEMINI_CLI_USER_AGENT,
|
||||||
|
'X-Goog-Api-Client': GEMINI_CLI_GOOGLE_API_CLIENT,
|
||||||
|
},
|
||||||
|
}, true),
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text().catch(() => '');
|
||||||
|
throw new Error(`HTTP ${response.status}: ${text || 'gemini cli oauth validation failed'}`);
|
||||||
|
}
|
||||||
|
const payload = await response.json() as { state?: unknown };
|
||||||
|
if (String(payload.state || '').trim().toUpperCase() !== 'ENABLED') {
|
||||||
|
throw new Error(`Cloud AI API not enabled for project ${projectId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function discoverAntigravityModelsFromCloud(input: {
|
||||||
|
site: PlatformDiscoverySite;
|
||||||
|
account: PlatformDiscoveryAccount;
|
||||||
|
}): Promise<string[]> {
|
||||||
|
const accessToken = (input.account.accessToken || '').trim();
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('antigravity oauth access token missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauth = getOauthInfoFromAccount(input.account);
|
||||||
|
const requestBody = oauth?.projectId ? { project: oauth.projectId } : {};
|
||||||
|
let lastError = '';
|
||||||
|
|
||||||
|
for (const baseUrl of buildAntigravityDiscoveryBaseUrls(input.site.url || ANTIGRAVITY_UPSTREAM_BASE_URL)) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${baseUrl}/v1internal:fetchAvailableModels`,
|
||||||
|
withSiteRecordProxyRequestInit(input.site, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': ANTIGRAVITY_MODELS_USER_AGENT,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
lastError = await response.text().catch(() => '') || `HTTP ${response.status}`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await response.json();
|
||||||
|
const models = normalizeDiscoveredModels(extractAntigravityModelIds(payload));
|
||||||
|
if (models.length > 0) {
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
lastError = '未获取到可用模型';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(lastError || '未获取到可用模型');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user