@rigelbuild/omp-catalog 18.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1204 -0
- package/LICENSE +22 -0
- package/README.md +43 -0
- package/THIRD-PARTY-NOTICES.txt +22909 -0
- package/dist/types/build.d.ts +19 -0
- package/dist/types/compat/anthropic.d.ts +18 -0
- package/dist/types/compat/apply.d.ts +7 -0
- package/dist/types/compat/bedrock.d.ts +3 -0
- package/dist/types/compat/devin.d.ts +9 -0
- package/dist/types/compat/openai.d.ts +28 -0
- package/dist/types/discovery/antigravity.d.ts +70 -0
- package/dist/types/discovery/codex.d.ts +36 -0
- package/dist/types/discovery/cursor-proto.d.ts +5368 -0
- package/dist/types/discovery/cursor.d.ts +23 -0
- package/dist/types/discovery/devin-proto.d.ts +1549 -0
- package/dist/types/discovery/devin.d.ts +24 -0
- package/dist/types/discovery/gemini-cli.d.ts +33 -0
- package/dist/types/discovery/gemini.d.ts +25 -0
- package/dist/types/discovery/gitlab-duo-workflow.d.ts +30 -0
- package/dist/types/discovery/index.d.ts +7 -0
- package/dist/types/discovery/openai-compatible.d.ts +89 -0
- package/dist/types/discovery/protobuf.d.ts +102 -0
- package/dist/types/effort.d.ts +10 -0
- package/dist/types/fireworks-model-id.d.ts +23 -0
- package/dist/types/hosts.d.ts +169 -0
- package/dist/types/identity/bundled.d.ts +3 -0
- package/dist/types/identity/classify.d.ts +69 -0
- package/dist/types/identity/dialect.d.ts +3 -0
- package/dist/types/identity/family.d.ts +239 -0
- package/dist/types/identity/id.d.ts +12 -0
- package/dist/types/identity/index.d.ts +8 -0
- package/dist/types/identity/markers.d.ts +4 -0
- package/dist/types/identity/priority.d.ts +1 -0
- package/dist/types/identity/reference.d.ts +29 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/model-cache.d.ts +23 -0
- package/dist/types/model-manager.d.ts +77 -0
- package/dist/types/model-thinking.d.ts +70 -0
- package/dist/types/model-tokenizer.d.ts +8 -0
- package/dist/types/models.d.ts +14 -0
- package/dist/types/openai-pricing.d.ts +17 -0
- package/dist/types/provider-models/bundled-references.d.ts +13 -0
- package/dist/types/provider-models/cache-provider-id.d.ts +11 -0
- package/dist/types/provider-models/descriptor-types.d.ts +76 -0
- package/dist/types/provider-models/descriptors.d.ts +505 -0
- package/dist/types/provider-models/google.d.ts +29 -0
- package/dist/types/provider-models/index.d.ts +7 -0
- package/dist/types/provider-models/ollama.d.ts +22 -0
- package/dist/types/provider-models/openai-compat.d.ts +646 -0
- package/dist/types/provider-models/special.d.ts +53 -0
- package/dist/types/types.d.ts +876 -0
- package/dist/types/utils.d.ts +20 -0
- package/dist/types/variant-collapse.d.ts +137 -0
- package/dist/types/wire/alibaba-token-plan.d.ts +22 -0
- package/dist/types/wire/codex.d.ts +66 -0
- package/dist/types/wire/coreweave.d.ts +12 -0
- package/dist/types/wire/gemini-headers.d.ts +62 -0
- package/dist/types/wire/github-copilot.d.ts +47 -0
- package/dist/types/wire/image-fetchers.d.ts +76 -0
- package/package.json +102 -0
- package/src/build.ts +103 -0
- package/src/compat/anthropic.ts +179 -0
- package/src/compat/apply.ts +15 -0
- package/src/compat/bedrock.ts +153 -0
- package/src/compat/devin.ts +12 -0
- package/src/compat/openai.ts +864 -0
- package/src/discovery/antigravity.ts +257 -0
- package/src/discovery/codex.ts +456 -0
- package/src/discovery/cursor-proto.ts +8329 -0
- package/src/discovery/cursor.ts +409 -0
- package/src/discovery/devin-proto.ts +2037 -0
- package/src/discovery/devin.ts +152 -0
- package/src/discovery/gemini-cli.ts +198 -0
- package/src/discovery/gemini.ts +260 -0
- package/src/discovery/gitlab-duo-workflow.ts +859 -0
- package/src/discovery/index.ts +7 -0
- package/src/discovery/openai-compatible.ts +266 -0
- package/src/discovery/protobuf.ts +1073 -0
- package/src/effort.ts +18 -0
- package/src/fireworks-model-id.ts +50 -0
- package/src/hosts.ts +178 -0
- package/src/identity/bundled.ts +28 -0
- package/src/identity/classify.ts +242 -0
- package/src/identity/dialect.ts +42 -0
- package/src/identity/family.ts +477 -0
- package/src/identity/id.ts +81 -0
- package/src/identity/index.ts +8 -0
- package/src/identity/markers.ts +49 -0
- package/src/identity/priority.ts +59 -0
- package/src/identity/reference.ts +167 -0
- package/src/index.ts +19 -0
- package/src/model-cache.ts +346 -0
- package/src/model-manager.ts +683 -0
- package/src/model-thinking.ts +925 -0
- package/src/model-tokenizer.ts +75 -0
- package/src/models.json +304657 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +108 -0
- package/src/openai-pricing.ts +29 -0
- package/src/provider-models/bundled-references.ts +87 -0
- package/src/provider-models/cache-provider-id.ts +97 -0
- package/src/provider-models/descriptor-types.ts +85 -0
- package/src/provider-models/descriptors.ts +585 -0
- package/src/provider-models/google.ts +117 -0
- package/src/provider-models/index.ts +7 -0
- package/src/provider-models/ollama.ts +204 -0
- package/src/provider-models/openai-compat.ts +6405 -0
- package/src/provider-models/special.ts +213 -0
- package/src/types.ts +1032 -0
- package/src/utils.ts +69 -0
- package/src/variant-collapse.ts +1555 -0
- package/src/wire/alibaba-token-plan.ts +54 -0
- package/src/wire/codex.ts +117 -0
- package/src/wire/coreweave.ts +51 -0
- package/src/wire/gemini-headers.ts +135 -0
- package/src/wire/github-copilot.ts +147 -0
- package/src/wire/image-fetchers.ts +159 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { gunzipSync } from "node:zlib";
|
|
2
|
+
import type { FetchImpl, ModelSpec } from "../types";
|
|
3
|
+
import { discoveryFetch } from "../utils";
|
|
4
|
+
import {
|
|
5
|
+
type ClientModelConfig,
|
|
6
|
+
GetCliModelConfigsRequestSchema,
|
|
7
|
+
GetCliModelConfigsResponseSchema,
|
|
8
|
+
MetadataSchema,
|
|
9
|
+
} from "./devin-proto";
|
|
10
|
+
import { create, fromBinary, toBinary } from "./protobuf";
|
|
11
|
+
|
|
12
|
+
const DEVIN_DEFAULT_BASE_URL = "https://server.codeium.com";
|
|
13
|
+
const DEVIN_GET_CLI_MODEL_CONFIGS_PATH = "/exa.api_server_pb.ApiServerService/GetCliModelConfigs";
|
|
14
|
+
const DEVIN_IDE_VERSION = "3.2.23";
|
|
15
|
+
const DEVIN_EXTENSION_VERSION = "1.48.2";
|
|
16
|
+
const DEVIN_SESSION_TOKEN_PREFIX = "devin-session-token$";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
19
|
+
const DEFAULT_MAX_TOKENS = 64_000;
|
|
20
|
+
|
|
21
|
+
/** Best-effort match for labels whose wording implies a thinking / reasoning-effort variant. */
|
|
22
|
+
const REASONING_LABEL_PATTERN = /think|thinking|minimal|high|medium|low|xhigh|max|reasoning/i;
|
|
23
|
+
const NO_REASONING_LABEL_PATTERN = /\bno thinking\b/i;
|
|
24
|
+
function supportsDevinThinking(config: ClientModelConfig): boolean {
|
|
25
|
+
if (NO_REASONING_LABEL_PATTERN.test(config.label)) return false;
|
|
26
|
+
return config.modelInfo?.modelFeatures?.supportsThinking === true || REASONING_LABEL_PATTERN.test(config.label);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Options for fetching dynamic Devin (Codeium Cascade) models from `GetCliModelConfigs`.
|
|
31
|
+
*/
|
|
32
|
+
export interface DevinModelDiscoveryOptions {
|
|
33
|
+
/** Codeium session token carried inside protobuf `Metadata.apiKey`. */
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
/** Optional Codeium API base URL override. */
|
|
36
|
+
baseUrl?: string;
|
|
37
|
+
/** Optional request timeout in milliseconds (default 5000). */
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
/** Optional caller abort signal, combined with the internal timeout. */
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
/** Optional fetch implementation for request-debug/proxy/test transports. */
|
|
42
|
+
fetch?: FetchImpl;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fetches Devin models through the `GetCliModelConfigs` unary Connect RPC and
|
|
47
|
+
* normalizes them into canonical model entries.
|
|
48
|
+
*
|
|
49
|
+
* Returns `null` on request/decode failures.
|
|
50
|
+
* Returns `[]` only when the endpoint responds successfully with no usable models.
|
|
51
|
+
*/
|
|
52
|
+
export async function fetchDevinModels(
|
|
53
|
+
options: DevinModelDiscoveryOptions,
|
|
54
|
+
): Promise<ModelSpec<"devin-agent">[] | null> {
|
|
55
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
56
|
+
const resolvedBaseUrl = options.baseUrl ?? DEVIN_DEFAULT_BASE_URL;
|
|
57
|
+
const requestUrl = `${resolvedBaseUrl.replace(/\/+$/, "")}${DEVIN_GET_CLI_MODEL_CONFIGS_PATH}`;
|
|
58
|
+
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
61
|
+
const signal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const request = create(GetCliModelConfigsRequestSchema, {
|
|
65
|
+
metadata: create(MetadataSchema, {
|
|
66
|
+
apiKey: normalizeDevinSessionToken(options.apiKey),
|
|
67
|
+
ideName: "windsurf",
|
|
68
|
+
ideVersion: DEVIN_IDE_VERSION,
|
|
69
|
+
extensionName: "windsurf",
|
|
70
|
+
extensionVersion: DEVIN_EXTENSION_VERSION,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
const body = toBinary(GetCliModelConfigsRequestSchema, request);
|
|
74
|
+
|
|
75
|
+
const headers: Record<string, string> = {
|
|
76
|
+
"content-type": "application/proto",
|
|
77
|
+
"connect-protocol-version": "1",
|
|
78
|
+
accept: "*/*",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
82
|
+
const response = await fetchImpl(requestUrl, { method: "POST", headers, body, signal });
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const decoded = decodeCliModelConfigsResponse(new Uint8Array(await response.arrayBuffer()));
|
|
88
|
+
if (!decoded) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return normalizeDevinModels(decoded.clientModelConfigs, options.baseUrl);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
} finally {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeDevinSessionToken(apiKey: string | undefined): string {
|
|
101
|
+
if (!apiKey) return "";
|
|
102
|
+
return apiKey.startsWith(DEVIN_SESSION_TOKEN_PREFIX) ? apiKey : `${DEVIN_SESSION_TOKEN_PREFIX}${apiKey}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Decodes a raw (unframed) `GetCliModelConfigsResponse`. Bun's `fetch` usually
|
|
107
|
+
* auto-decompresses gzip, so the direct decode is attempted first; a
|
|
108
|
+
* `gunzipSync` fallback covers runtimes that hand back the still-compressed body.
|
|
109
|
+
*/
|
|
110
|
+
function decodeCliModelConfigsResponse(payload: Uint8Array) {
|
|
111
|
+
try {
|
|
112
|
+
return fromBinary(GetCliModelConfigsResponseSchema, payload);
|
|
113
|
+
} catch {
|
|
114
|
+
try {
|
|
115
|
+
return fromBinary(GetCliModelConfigsResponseSchema, gunzipSync(payload));
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeDevinModels(
|
|
123
|
+
configs: readonly ClientModelConfig[],
|
|
124
|
+
baseUrlOverride: string | undefined,
|
|
125
|
+
): ModelSpec<"devin-agent">[] {
|
|
126
|
+
const byId = new Map<string, ModelSpec<"devin-agent">>();
|
|
127
|
+
for (const config of configs) {
|
|
128
|
+
if (config.disabled) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const id = config.modelUid.trim();
|
|
132
|
+
if (!id) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const input: ("text" | "image")[] = config.supportsImages ? ["text", "image"] : ["text"];
|
|
136
|
+
const contextWindow = config.maxTokens > 0 ? config.maxTokens : DEFAULT_CONTEXT_WINDOW;
|
|
137
|
+
byId.set(id, {
|
|
138
|
+
id,
|
|
139
|
+
name: config.label.trim() || id,
|
|
140
|
+
api: "devin-agent",
|
|
141
|
+
provider: "devin",
|
|
142
|
+
baseUrl: baseUrlOverride ?? DEVIN_DEFAULT_BASE_URL,
|
|
143
|
+
reasoning: supportsDevinThinking(config),
|
|
144
|
+
input,
|
|
145
|
+
supportsTools: true,
|
|
146
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
147
|
+
contextWindow,
|
|
148
|
+
maxTokens: Math.min(config.maxTokens > 0 ? config.maxTokens : DEFAULT_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
152
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { type } from "@rigelbuild/omp-omptype";
|
|
2
|
+
import type { FetchImpl } from "@rigelbuild/omp-utils";
|
|
3
|
+
import { parseGeminiModel, semverGte } from "../identity/classify";
|
|
4
|
+
import { isGeminiModelId } from "../identity/family";
|
|
5
|
+
import { createBundledReferenceMap } from "../provider-models/bundled-references";
|
|
6
|
+
import type { ModelSpec } from "../types";
|
|
7
|
+
import { discoveryFetch } from "../utils";
|
|
8
|
+
import {
|
|
9
|
+
collapseEffortVariants,
|
|
10
|
+
GEMINI_CLI_VARIANT_COLLAPSE_TABLE,
|
|
11
|
+
type VariantCollapseTable,
|
|
12
|
+
} from "../variant-collapse";
|
|
13
|
+
import { getGeminiCliHeaders } from "../wire/gemini-headers";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
16
|
+
const LOAD_CODE_ASSIST_PATH = "/v1internal:loadCodeAssist";
|
|
17
|
+
const RETRIEVE_USER_QUOTA_PATH = "/v1internal:retrieveUserQuota";
|
|
18
|
+
|
|
19
|
+
// All current Gemini CLI models ship a 1M-token context and 65,536-token
|
|
20
|
+
// output ceiling; used only for quota-listed ids the bundled catalog does not
|
|
21
|
+
// already describe. Ids present in the bundle keep their real limits.
|
|
22
|
+
const DEFAULT_CONTEXT_WINDOW = 1_048_576;
|
|
23
|
+
const DEFAULT_MAX_TOKENS = 65_536;
|
|
24
|
+
|
|
25
|
+
/** Gemini generations that expose thinking on Cloud Code Assist. */
|
|
26
|
+
const REASONING_MIN_VERSION = "2.5";
|
|
27
|
+
|
|
28
|
+
const LoadCodeAssistResponseSchema = type({
|
|
29
|
+
"cloudaicompanionProject?": type("unknown").pipe(value => {
|
|
30
|
+
if (typeof value === "string") return value;
|
|
31
|
+
if (value && typeof value === "object" && "id" in value && typeof value.id === "string") {
|
|
32
|
+
return value.id;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const QuotaBucketSchema = type({
|
|
39
|
+
"modelId?": type("unknown").pipe(value => (typeof value === "string" ? value : undefined)),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const RetrieveUserQuotaResponseSchema = type({
|
|
43
|
+
"buckets?": type("unknown").pipe(value => {
|
|
44
|
+
if (!Array.isArray(value)) return undefined;
|
|
45
|
+
const buckets: Array<{ modelId?: string }> = [];
|
|
46
|
+
for (const bucket of value) {
|
|
47
|
+
const parsed = QuotaBucketSchema(bucket);
|
|
48
|
+
if (!(parsed instanceof type.errors)) {
|
|
49
|
+
buckets.push(parsed);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return buckets;
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Options for the Gemini CLI quota-based discovery fallback.
|
|
58
|
+
*/
|
|
59
|
+
export interface FetchGeminiCliQuotaModelsOptions {
|
|
60
|
+
/** OAuth access token sent as `Authorization: Bearer <token>`. */
|
|
61
|
+
token: string;
|
|
62
|
+
/** Cloud Code Assist endpoint. Defaults to `https://cloudcode-pa.googleapis.com`. */
|
|
63
|
+
endpoint?: string;
|
|
64
|
+
/** Pre-resolved GCP project id; otherwise discovered via `loadCodeAssist`. */
|
|
65
|
+
projectId?: string;
|
|
66
|
+
/** Optional abort signal for request cancellation. */
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
/** Optional fetch implementation override for tests. */
|
|
69
|
+
fetcher?: typeof fetch;
|
|
70
|
+
/** Effort-tier collapse table applied to the discovered list. */
|
|
71
|
+
collapseTable?: VariantCollapseTable;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Discovers the Gemini models available to a `google-gemini-cli` credential via
|
|
76
|
+
* the account's own `retrieveUserQuota` endpoint on Cloud Code Assist.
|
|
77
|
+
*
|
|
78
|
+
* This is the fallback for accounts whose credential is not authorized for the
|
|
79
|
+
* Antigravity `fetchAvailableModels` endpoint (e.g. Gemini Code Assist Standard
|
|
80
|
+
* tiers, which return HTTP 403 there). Quota buckets carry only model ids, so
|
|
81
|
+
* metadata is filled from the bundled catalog where the id is known and
|
|
82
|
+
* synthesized with Gemini CLI defaults otherwise.
|
|
83
|
+
*
|
|
84
|
+
* Returns `null` on network/payload/auth failure (the caller keeps the bundled
|
|
85
|
+
* catalog). Returns `[]` when the quota response lists no usable Gemini models.
|
|
86
|
+
*/
|
|
87
|
+
export async function fetchGeminiCliQuotaModels(
|
|
88
|
+
options: FetchGeminiCliQuotaModelsOptions,
|
|
89
|
+
): Promise<ModelSpec<"google-gemini-cli">[] | null> {
|
|
90
|
+
const fetcher = discoveryFetch(options.fetcher);
|
|
91
|
+
const endpoint = (options.endpoint?.trim() || DEFAULT_ENDPOINT).replace(/\/+$/, "");
|
|
92
|
+
const headers = {
|
|
93
|
+
Authorization: `Bearer ${options.token}`,
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
...getGeminiCliHeaders(),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const projectId = options.projectId ?? (await loadProjectId(fetcher, endpoint, headers, options.signal));
|
|
99
|
+
|
|
100
|
+
let response: Response;
|
|
101
|
+
try {
|
|
102
|
+
response = await fetcher(`${endpoint}${RETRIEVE_USER_QUOTA_PATH}`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers,
|
|
105
|
+
body: JSON.stringify(projectId ? { project: projectId } : {}),
|
|
106
|
+
signal: options.signal,
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let payload: unknown;
|
|
117
|
+
try {
|
|
118
|
+
payload = await response.json();
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const parsed = RetrieveUserQuotaResponseSchema(payload);
|
|
124
|
+
if (parsed instanceof type.errors) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const seen = new Set<string>();
|
|
129
|
+
const models: ModelSpec<"google-gemini-cli">[] = [];
|
|
130
|
+
const bundled = createBundledReferenceMap<"google-gemini-cli">("google-gemini-cli");
|
|
131
|
+
|
|
132
|
+
for (const bucket of parsed.buckets ?? []) {
|
|
133
|
+
const modelId = bucket.modelId?.trim();
|
|
134
|
+
if (!modelId || seen.has(modelId) || !isGeminiModelId(modelId)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
seen.add(modelId);
|
|
138
|
+
|
|
139
|
+
const reference = bundled.get(modelId);
|
|
140
|
+
if (reference) {
|
|
141
|
+
models.push({ ...reference, baseUrl: endpoint });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const parsedId = parseGeminiModel(modelId);
|
|
146
|
+
models.push({
|
|
147
|
+
id: modelId,
|
|
148
|
+
name: modelId,
|
|
149
|
+
api: "google-gemini-cli",
|
|
150
|
+
provider: "google-gemini-cli",
|
|
151
|
+
baseUrl: endpoint,
|
|
152
|
+
reasoning: parsedId ? semverGte(parsedId.version, REASONING_MIN_VERSION) : false,
|
|
153
|
+
input: ["text", "image"],
|
|
154
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
155
|
+
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
156
|
+
maxTokens: DEFAULT_MAX_TOKENS,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const collapsed = collapseEffortVariants(models, options.collapseTable ?? GEMINI_CLI_VARIANT_COLLAPSE_TABLE);
|
|
161
|
+
collapsed.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
162
|
+
return collapsed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function loadProjectId(
|
|
166
|
+
fetcher: FetchImpl,
|
|
167
|
+
endpoint: string,
|
|
168
|
+
headers: Record<string, string>,
|
|
169
|
+
signal: AbortSignal | undefined,
|
|
170
|
+
): Promise<string | undefined> {
|
|
171
|
+
let response: Response;
|
|
172
|
+
try {
|
|
173
|
+
response = await fetcher(`${endpoint}${LOAD_CODE_ASSIST_PATH}`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers,
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
metadata: { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" },
|
|
178
|
+
}),
|
|
179
|
+
signal,
|
|
180
|
+
});
|
|
181
|
+
} catch {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let payload: unknown;
|
|
190
|
+
try {
|
|
191
|
+
payload = await response.json();
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const parsed = LoadCodeAssistResponseSchema(payload);
|
|
197
|
+
return parsed instanceof type.errors ? undefined : parsed.cloudaicompanionProject;
|
|
198
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { type } from "@rigelbuild/omp-omptype";
|
|
2
|
+
import { getBundledModels } from "../models";
|
|
3
|
+
import { toModelSpec } from "../provider-models/bundled-references";
|
|
4
|
+
import type { FetchImpl, Model, ModelSpec } from "../types";
|
|
5
|
+
import { discoveryFetch } from "../utils";
|
|
6
|
+
|
|
7
|
+
const GOOGLE_GENERATIVE_AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
8
|
+
const DEFAULT_PAGE_SIZE = 100;
|
|
9
|
+
const DEFAULT_MAX_PAGES = 25;
|
|
10
|
+
|
|
11
|
+
const resilientString = type("unknown").pipe(val => {
|
|
12
|
+
if (val === undefined) return undefined;
|
|
13
|
+
const out = type("string")(val);
|
|
14
|
+
return out instanceof type.errors ? undefined : out;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const resilientNumber = type("unknown").pipe(val => {
|
|
18
|
+
if (val === undefined) return undefined;
|
|
19
|
+
const out = type("number")(val);
|
|
20
|
+
return out instanceof type.errors ? undefined : out;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const geminiModelListItemSchema = type({
|
|
24
|
+
"name?": resilientString,
|
|
25
|
+
"displayName?": resilientString,
|
|
26
|
+
"supportedGenerationMethods?": "string[]",
|
|
27
|
+
"inputTokenLimit?": resilientNumber,
|
|
28
|
+
"outputTokenLimit?": resilientNumber,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
type GeminiModelListItem = typeof geminiModelListItemSchema.infer;
|
|
32
|
+
|
|
33
|
+
const modelsSchema = type("unknown[]")
|
|
34
|
+
.pipe(items => {
|
|
35
|
+
const parsedItems: GeminiModelListItem[] = [];
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
const parsed = geminiModelListItemSchema(item);
|
|
38
|
+
if (!(parsed instanceof type.errors)) {
|
|
39
|
+
parsedItems.push(parsed);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return parsedItems;
|
|
43
|
+
})
|
|
44
|
+
.default(() => []);
|
|
45
|
+
|
|
46
|
+
const geminiModelListResponseSchema = type({
|
|
47
|
+
models: modelsSchema,
|
|
48
|
+
"nextPageToken?": resilientString,
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Configuration for Google Generative AI model discovery.
|
|
52
|
+
*/
|
|
53
|
+
export interface GeminiDiscoveryOptions {
|
|
54
|
+
/** API key for the Google Generative AI public endpoint. */
|
|
55
|
+
apiKey: string;
|
|
56
|
+
/** Optional endpoint override for testing or proxying. */
|
|
57
|
+
baseUrl?: string;
|
|
58
|
+
/** Optional requested page size for model listing. */
|
|
59
|
+
pageSize?: number;
|
|
60
|
+
/** Maximum number of pages to request before stopping pagination. */
|
|
61
|
+
maxPages?: number;
|
|
62
|
+
/** Optional abort signal for HTTP requests. */
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
/** Optional fetch implementation override for tests. */
|
|
65
|
+
fetch?: FetchImpl;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fetches and normalizes Google Generative AI models from the public models endpoint.
|
|
70
|
+
*
|
|
71
|
+
* Returns `null` on transport/protocol failures.
|
|
72
|
+
* Returns `[]` only when the endpoint responds successfully with no usable models.
|
|
73
|
+
*/
|
|
74
|
+
export async function fetchGeminiModels(
|
|
75
|
+
options: GeminiDiscoveryOptions,
|
|
76
|
+
): Promise<ModelSpec<"google-generative-ai">[] | null> {
|
|
77
|
+
if (!options.apiKey.trim()) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
82
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
83
|
+
const pageSize = normalizePositiveInt(options.pageSize, DEFAULT_PAGE_SIZE);
|
|
84
|
+
const maxPages = normalizePositiveInt(options.maxPages, DEFAULT_MAX_PAGES);
|
|
85
|
+
|
|
86
|
+
const bundledById = new Map(
|
|
87
|
+
getBundledModels("google").map(model => [model.id, toModelSpec(model as Model<"google-generative-ai">)]),
|
|
88
|
+
);
|
|
89
|
+
const modelsById = new Map<string, ModelSpec<"google-generative-ai">>();
|
|
90
|
+
const seenTokens = new Set<string>();
|
|
91
|
+
let nextPageToken: string | undefined;
|
|
92
|
+
|
|
93
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
94
|
+
const requestUrl = buildModelsUrl(baseUrl, options.apiKey, pageSize, nextPageToken);
|
|
95
|
+
let response: Response;
|
|
96
|
+
try {
|
|
97
|
+
response = await fetchImpl(requestUrl, {
|
|
98
|
+
method: "GET",
|
|
99
|
+
signal: options.signal,
|
|
100
|
+
});
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let payload: unknown;
|
|
110
|
+
try {
|
|
111
|
+
payload = await response.json();
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const parsed = geminiModelListResponseSchema(payload);
|
|
117
|
+
if (parsed instanceof type.errors) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const item of parsed.models) {
|
|
122
|
+
const model = normalizeModel(item, baseUrl, bundledById);
|
|
123
|
+
if (model) {
|
|
124
|
+
modelsById.set(model.id, model);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const token = normalizePageToken(parsed.nextPageToken);
|
|
129
|
+
if (!token) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (seenTokens.has(token)) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
seenTokens.add(token);
|
|
136
|
+
nextPageToken = token;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return Array.from(modelsById.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildModelsUrl(baseUrl: string, apiKey: string, pageSize: number, pageToken?: string): URL {
|
|
143
|
+
const url = new URL(`${baseUrl}/models`);
|
|
144
|
+
url.searchParams.set("key", apiKey);
|
|
145
|
+
url.searchParams.set("pageSize", String(pageSize));
|
|
146
|
+
if (pageToken) {
|
|
147
|
+
url.searchParams.set("pageToken", pageToken);
|
|
148
|
+
}
|
|
149
|
+
return url;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeBaseUrl(baseUrl?: string): string {
|
|
153
|
+
const value = (baseUrl ?? GOOGLE_GENERATIVE_AI_BASE_URL).trim();
|
|
154
|
+
if (!value) {
|
|
155
|
+
return GOOGLE_GENERATIVE_AI_BASE_URL;
|
|
156
|
+
}
|
|
157
|
+
return value.replace(/\/+$/, "");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizePositiveInt(value: number | undefined, fallback: number): number;
|
|
161
|
+
function normalizePositiveInt(value: number | undefined, fallback: number | null): number | null;
|
|
162
|
+
function normalizePositiveInt(value: number | undefined, fallback: number | null): number | null {
|
|
163
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
164
|
+
return fallback;
|
|
165
|
+
}
|
|
166
|
+
const normalized = Math.floor(value);
|
|
167
|
+
return normalized > 0 ? normalized : fallback;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizePageToken(value: unknown): string | undefined {
|
|
171
|
+
if (typeof value !== "string") {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const token = value.trim();
|
|
175
|
+
return token.length > 0 ? token : undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeModel(
|
|
179
|
+
item: GeminiModelListItem,
|
|
180
|
+
baseUrl: string,
|
|
181
|
+
bundledById: Map<string, ModelSpec<"google-generative-ai">>,
|
|
182
|
+
): ModelSpec<"google-generative-ai"> | null {
|
|
183
|
+
const id = normalizeModelId(item.name);
|
|
184
|
+
if (!id) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (!supportsTextGeneration(item.supportedGenerationMethods)) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const reference = bundledById.get(id);
|
|
192
|
+
const contextWindow = normalizePositiveInt(item.inputTokenLimit, reference?.contextWindow ?? null);
|
|
193
|
+
const maxTokens = normalizePositiveInt(item.outputTokenLimit, reference?.maxTokens ?? null);
|
|
194
|
+
const name = normalizeModelName(item.displayName, reference?.name ?? id);
|
|
195
|
+
|
|
196
|
+
if (reference) {
|
|
197
|
+
return {
|
|
198
|
+
...reference,
|
|
199
|
+
id,
|
|
200
|
+
name,
|
|
201
|
+
baseUrl,
|
|
202
|
+
contextWindow,
|
|
203
|
+
maxTokens,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
id,
|
|
208
|
+
name,
|
|
209
|
+
api: "google-generative-ai",
|
|
210
|
+
provider: "google",
|
|
211
|
+
baseUrl,
|
|
212
|
+
reasoning: inferReasoningFromGeminiId(id),
|
|
213
|
+
input: inferInputFromGeminiId(id),
|
|
214
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
215
|
+
contextWindow,
|
|
216
|
+
maxTokens,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeModelId(value: string | undefined): string | null {
|
|
221
|
+
if (!value) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const trimmed = value.trim();
|
|
225
|
+
if (!trimmed) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return trimmed.startsWith("models/") ? trimmed.slice("models/".length) : trimmed;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function normalizeModelName(displayName: string | undefined, id: string): string {
|
|
232
|
+
const trimmed = displayName?.trim();
|
|
233
|
+
return trimmed ? trimmed : id;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function supportsTextGeneration(methods: string[] | undefined): boolean {
|
|
237
|
+
if (!methods || methods.length === 0) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return methods.some(method => method === "generateContent");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function inferReasoningFromGeminiId(id: string): boolean {
|
|
244
|
+
const normalized = id.toLowerCase();
|
|
245
|
+
if (normalized.includes("thinking")) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
if (normalized.includes("pro") || normalized.includes("2.5")) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function inferInputFromGeminiId(id: string): ("text" | "image")[] {
|
|
255
|
+
const normalized = id.toLowerCase();
|
|
256
|
+
if (normalized.includes("vision") || normalized.includes("image") || normalized.includes("gemini")) {
|
|
257
|
+
return ["text", "image"];
|
|
258
|
+
}
|
|
259
|
+
return ["text"];
|
|
260
|
+
}
|