@oh-my-pi/pi-catalog 16.1.23 → 16.2.1
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 +18 -0
- package/README.md +1 -1
- package/dist/types/discovery/gitlab-duo-workflow.d.ts +30 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/provider-models/descriptor-types.d.ts +1 -1
- package/dist/types/provider-models/descriptors.d.ts +6 -0
- package/dist/types/provider-models/openai-compat.d.ts +15 -1
- package/dist/types/provider-models/special.d.ts +10 -0
- package/dist/types/types.d.ts +28 -2
- package/package.json +4 -4
- package/src/compat/openai.ts +2 -0
- package/src/discovery/gitlab-duo-workflow.ts +855 -0
- package/src/discovery/index.ts +1 -0
- package/src/models.json +286 -141
- package/src/provider-models/descriptor-types.ts +1 -1
- package/src/provider-models/descriptors.ts +13 -1
- package/src/provider-models/openai-compat.ts +247 -11
- package/src/provider-models/special.ts +64 -1
- package/src/types.ts +29 -0
|
@@ -55,7 +55,7 @@ export function allowsUnauthenticatedCatalogDiscovery(descriptor: CatalogProvide
|
|
|
55
55
|
* - Every entry is a member of `KnownProvider`.
|
|
56
56
|
* - `createModelManagerOptions` present (and not `specialModelManager`) ⇒
|
|
57
57
|
* appears in `PROVIDER_DESCRIPTORS` for runtime model discovery.
|
|
58
|
-
* -
|
|
58
|
+
* - \`catalogDiscovery\` present ⇒ participates in \`generate-models.ts\`.
|
|
59
59
|
*/
|
|
60
60
|
export interface ProviderCatalogEntry {
|
|
61
61
|
readonly id: string;
|
|
@@ -50,7 +50,12 @@ import {
|
|
|
50
50
|
zenmuxModelManagerOptions,
|
|
51
51
|
zhipuCodingPlanModelManagerOptions,
|
|
52
52
|
} from "./openai-compat";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
cursorModelManagerOptions,
|
|
55
|
+
devinModelManagerOptions,
|
|
56
|
+
gitLabDuoWorkflowModelManagerOptions,
|
|
57
|
+
zaiModelManagerOptions,
|
|
58
|
+
} from "./special";
|
|
54
59
|
|
|
55
60
|
export const CATALOG_PROVIDERS = [
|
|
56
61
|
{
|
|
@@ -142,6 +147,13 @@ export const CATALOG_PROVIDERS = [
|
|
|
142
147
|
defaultModel: "duo-chat-opus-4-6",
|
|
143
148
|
envVars: ["GITLAB_TOKEN"],
|
|
144
149
|
},
|
|
150
|
+
{
|
|
151
|
+
id: "gitlab-duo-agent",
|
|
152
|
+
defaultModel: "claude_sonnet_4_6_vertex",
|
|
153
|
+
envVars: ["GITLAB_TOKEN"],
|
|
154
|
+
createModelManagerOptions: (config: ModelManagerConfig) => gitLabDuoWorkflowModelManagerOptions(config),
|
|
155
|
+
dynamicModelsAuthoritative: true,
|
|
156
|
+
},
|
|
145
157
|
{
|
|
146
158
|
id: "google",
|
|
147
159
|
defaultModel: "gemini-3.1-pro-preview",
|
|
@@ -8,7 +8,7 @@ import { FIREWORKS_FAST_SUFFIX, toFireworksPublicModelId } from "../fireworks-mo
|
|
|
8
8
|
import { isGlmVisionModelId, isGrokReasoningEffortCapable, isReasoningGlmModelId } from "../identity/family";
|
|
9
9
|
import type { ModelManagerOptions } from "../model-manager";
|
|
10
10
|
import { getBundledModels } from "../models";
|
|
11
|
-
import type { Api, FetchImpl, Model, ModelSpec, Provider, ThinkingConfig } from "../types";
|
|
11
|
+
import type { Api, FetchImpl, Model, ModelSpec, OpenAICompat, Provider, ThinkingConfig } from "../types";
|
|
12
12
|
import { isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
|
|
13
13
|
import { coreWeaveProjectHeaders } from "../wire/coreweave";
|
|
14
14
|
import {
|
|
@@ -1066,7 +1066,7 @@ function applyXAIOAuthCuration(dynamic: readonly ModelSpec<"openai-responses">[]
|
|
|
1066
1066
|
* Single source of truth for the curated to Model fan-in, consumed by both
|
|
1067
1067
|
* - {@link xaiOAuthModelManagerOptions} (runtime static seed handed to the model
|
|
1068
1068
|
* manager so the picker is populated on a fresh login), and
|
|
1069
|
-
* -
|
|
1069
|
+
* - \`packages/catalog/scripts/generate-models.ts\` (bundles the same entries into
|
|
1070
1070
|
* `models.json`, so the synchronous `ModelRegistry.#loadModels()` boot path
|
|
1071
1071
|
* sees `xai-oauth` without waiting for a refresh — fixes the boot-time
|
|
1072
1072
|
* default-model reset when `modelRoles.default = "xai-oauth/<id>"`).
|
|
@@ -1115,7 +1115,7 @@ export function xaiOAuthModelManagerOptions(
|
|
|
1115
1115
|
// Static seed handed to the runtime model manager so the picker populates on
|
|
1116
1116
|
// a fresh login even before `fetchDynamicModels` fires (it is gated on
|
|
1117
1117
|
// `config.apiKey` at construction time, and OAuth tokens resolve later via
|
|
1118
|
-
// AuthStorage).
|
|
1118
|
+
// AuthStorage). \`generate-models.ts\` calls the same builder so \`models.json\`
|
|
1119
1119
|
// carries these entries too — making the synchronous `#loadModels()` boot
|
|
1120
1120
|
// path honor `modelRoles.default = "xai-oauth/<id>"` without `await refresh()`.
|
|
1121
1121
|
const staticModels = buildXaiOAuthStaticSeed(resolvedBaseUrl);
|
|
@@ -2817,6 +2817,231 @@ export interface LiteLLMModelManagerConfig {
|
|
|
2817
2817
|
fetch?: FetchImpl;
|
|
2818
2818
|
}
|
|
2819
2819
|
|
|
2820
|
+
export interface FetchLiteLLMRichModelsOptions<TApi extends Api> {
|
|
2821
|
+
api: TApi;
|
|
2822
|
+
provider: Provider;
|
|
2823
|
+
baseUrl: string;
|
|
2824
|
+
apiKey?: string;
|
|
2825
|
+
headers?: Record<string, string>;
|
|
2826
|
+
fetch?: FetchImpl;
|
|
2827
|
+
signal?: AbortSignal;
|
|
2828
|
+
referenceResolver?: (modelId: string) => ModelSpec<TApi> | undefined;
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
type LiteLLMRichModelEntry = Record<string, unknown>;
|
|
2832
|
+
|
|
2833
|
+
const LITELLM_RICH_ENDPOINTS = ["/model_group/info", "/v2/model/info", "/model/info", "/v1/model/info"] as const;
|
|
2834
|
+
export const OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
2835
|
+
export const OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS = 32_768;
|
|
2836
|
+
const UNKNOWN_PROXY_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;
|
|
2837
|
+
|
|
2838
|
+
export function normalizeLiteLLMManagementBaseUrl(baseUrl: string): string {
|
|
2839
|
+
const trimmed = baseUrl.trim().replace(/\/+$/g, "");
|
|
2840
|
+
if (!trimmed) {
|
|
2841
|
+
return "";
|
|
2842
|
+
}
|
|
2843
|
+
try {
|
|
2844
|
+
const parsed = new URL(trimmed);
|
|
2845
|
+
const path = parsed.pathname.replace(/\/+$/g, "");
|
|
2846
|
+
parsed.pathname = path.endsWith("/v1") ? path.slice(0, -3) || "/" : path || "/";
|
|
2847
|
+
const normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
2848
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
2849
|
+
} catch {
|
|
2850
|
+
return trimmed.replace(/\/v1$/, "");
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
function normalizeLiteLLMRuntimeBaseUrl(baseUrl: string): string {
|
|
2855
|
+
const trimmed = baseUrl.trim();
|
|
2856
|
+
return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
function toNonEmptyString(value: unknown): string | undefined {
|
|
2860
|
+
if (typeof value !== "string") {
|
|
2861
|
+
return undefined;
|
|
2862
|
+
}
|
|
2863
|
+
const trimmed = value.trim();
|
|
2864
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
function extractLiteLLMRichEntries(payload: unknown): LiteLLMRichModelEntry[] | null {
|
|
2868
|
+
if (Array.isArray(payload)) {
|
|
2869
|
+
return payload.flatMap(entry => (isRecord(entry) ? [entry] : []));
|
|
2870
|
+
}
|
|
2871
|
+
if (!isRecord(payload)) {
|
|
2872
|
+
return null;
|
|
2873
|
+
}
|
|
2874
|
+
for (const candidate of [payload.data, payload.models, payload.result, payload.items]) {
|
|
2875
|
+
if (candidate === undefined) {
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2878
|
+
const entries = extractLiteLLMRichEntries(candidate);
|
|
2879
|
+
if (entries !== null) {
|
|
2880
|
+
return entries;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return null;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
function getLiteLLMModelInfo(entry: LiteLLMRichModelEntry): LiteLLMRichModelEntry | undefined {
|
|
2887
|
+
return isRecord(entry.model_info) ? entry.model_info : undefined;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
function getLiteLLMParams(entry: LiteLLMRichModelEntry): LiteLLMRichModelEntry | undefined {
|
|
2891
|
+
return isRecord(entry.litellm_params) ? entry.litellm_params : undefined;
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
function getLiteLLMMetadataValue(entry: LiteLLMRichModelEntry, key: string): unknown {
|
|
2895
|
+
return entry[key] ?? getLiteLLMModelInfo(entry)?.[key];
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
function getLiteLLMRichModelId(entry: LiteLLMRichModelEntry): string | undefined {
|
|
2899
|
+
return (
|
|
2900
|
+
toNonEmptyString(entry.model_group) ??
|
|
2901
|
+
toNonEmptyString(entry.model_name) ??
|
|
2902
|
+
toNonEmptyString(entry.id) ??
|
|
2903
|
+
toNonEmptyString(getLiteLLMParams(entry)?.model)
|
|
2904
|
+
);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
function getSupportedOpenAIParams(entry: LiteLLMRichModelEntry): string[] | undefined {
|
|
2908
|
+
const value = getLiteLLMMetadataValue(entry, "supported_openai_params");
|
|
2909
|
+
if (!Array.isArray(value)) {
|
|
2910
|
+
return undefined;
|
|
2911
|
+
}
|
|
2912
|
+
return value.flatMap(item => (typeof item === "string" ? [item] : []));
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
function mapLiteLLMRichEntry<TApi extends Api>(
|
|
2916
|
+
entry: LiteLLMRichModelEntry,
|
|
2917
|
+
options: FetchLiteLLMRichModelsOptions<TApi>,
|
|
2918
|
+
runtimeBaseUrl: string,
|
|
2919
|
+
): ModelSpec<TApi> | null {
|
|
2920
|
+
const id = getLiteLLMRichModelId(entry);
|
|
2921
|
+
if (!id) {
|
|
2922
|
+
return null;
|
|
2923
|
+
}
|
|
2924
|
+
const reference = options.referenceResolver?.(id);
|
|
2925
|
+
const modelName = toNonEmptyString(entry.model_name);
|
|
2926
|
+
const contextWindow = toPositiveNumber(
|
|
2927
|
+
getLiteLLMMetadataValue(entry, "max_input_tokens"),
|
|
2928
|
+
reference?.contextWindow ?? OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW,
|
|
2929
|
+
);
|
|
2930
|
+
const maxTokens = toPositiveNumber(
|
|
2931
|
+
getLiteLLMMetadataValue(entry, "max_output_tokens"),
|
|
2932
|
+
reference?.maxTokens ?? Math.min(contextWindow, OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
2933
|
+
);
|
|
2934
|
+
const supportsVision = getLiteLLMMetadataValue(entry, "supports_vision");
|
|
2935
|
+
const supportsReasoning = getLiteLLMMetadataValue(entry, "supports_reasoning");
|
|
2936
|
+
const supportedOpenAIParams = getSupportedOpenAIParams(entry);
|
|
2937
|
+
const supportsFunctionCalling = getLiteLLMMetadataValue(entry, "supports_function_calling");
|
|
2938
|
+
const supportsTools =
|
|
2939
|
+
supportsFunctionCalling === true
|
|
2940
|
+
? true
|
|
2941
|
+
: supportsFunctionCalling === false
|
|
2942
|
+
? false
|
|
2943
|
+
: supportedOpenAIParams !== undefined
|
|
2944
|
+
? supportedOpenAIParams.some(param =>
|
|
2945
|
+
["tools", "tool_choice", "functions", "function_call"].includes(param),
|
|
2946
|
+
)
|
|
2947
|
+
: reference?.supportsTools;
|
|
2948
|
+
const compat: OpenAICompat = {
|
|
2949
|
+
...(reference?.compat ?? {}),
|
|
2950
|
+
supportsStore: false,
|
|
2951
|
+
supportsDeveloperRole: false,
|
|
2952
|
+
...(supportedOpenAIParams !== undefined
|
|
2953
|
+
? { supportsReasoningEffort: supportedOpenAIParams.includes("reasoning_effort") }
|
|
2954
|
+
: {}),
|
|
2955
|
+
};
|
|
2956
|
+
return {
|
|
2957
|
+
id,
|
|
2958
|
+
name: modelName && modelName !== id ? modelName : (reference?.name ?? id),
|
|
2959
|
+
api: options.api,
|
|
2960
|
+
provider: options.provider,
|
|
2961
|
+
baseUrl: runtimeBaseUrl,
|
|
2962
|
+
contextWindow,
|
|
2963
|
+
maxTokens,
|
|
2964
|
+
input:
|
|
2965
|
+
supportsVision === true
|
|
2966
|
+
? ["text", "image"]
|
|
2967
|
+
: supportsVision === false
|
|
2968
|
+
? ["text"]
|
|
2969
|
+
: (reference?.input ?? ["text"]),
|
|
2970
|
+
reasoning: typeof supportsReasoning === "boolean" ? supportsReasoning : (reference?.reasoning ?? false),
|
|
2971
|
+
thinking: reference?.thinking,
|
|
2972
|
+
cost: reference?.cost ?? UNKNOWN_PROXY_COST,
|
|
2973
|
+
...(supportsTools !== undefined ? { supportsTools } : {}),
|
|
2974
|
+
compat: compat as ModelSpec<TApi>["compat"],
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
2979
|
+
endpoint: string,
|
|
2980
|
+
options: FetchLiteLLMRichModelsOptions<TApi>,
|
|
2981
|
+
managementBaseUrl: string,
|
|
2982
|
+
runtimeBaseUrl: string,
|
|
2983
|
+
): Promise<ModelSpec<TApi>[] | null> {
|
|
2984
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2985
|
+
const requestHeaders: Record<string, string> = {
|
|
2986
|
+
Accept: "application/json",
|
|
2987
|
+
...options.headers,
|
|
2988
|
+
};
|
|
2989
|
+
if (options.apiKey) {
|
|
2990
|
+
requestHeaders.Authorization = `Bearer ${options.apiKey}`;
|
|
2991
|
+
}
|
|
2992
|
+
let response: Response;
|
|
2993
|
+
try {
|
|
2994
|
+
response = await fetchImpl(`${managementBaseUrl}${endpoint}`, {
|
|
2995
|
+
method: "GET",
|
|
2996
|
+
headers: requestHeaders,
|
|
2997
|
+
signal: options.signal,
|
|
2998
|
+
});
|
|
2999
|
+
} catch {
|
|
3000
|
+
return null;
|
|
3001
|
+
}
|
|
3002
|
+
if (!response.ok) {
|
|
3003
|
+
return null;
|
|
3004
|
+
}
|
|
3005
|
+
let payload: unknown;
|
|
3006
|
+
try {
|
|
3007
|
+
payload = await response.json();
|
|
3008
|
+
} catch {
|
|
3009
|
+
return null;
|
|
3010
|
+
}
|
|
3011
|
+
const entries = extractLiteLLMRichEntries(payload);
|
|
3012
|
+
if (!entries || entries.length === 0) {
|
|
3013
|
+
return null;
|
|
3014
|
+
}
|
|
3015
|
+
const deduped = new Map<string, ModelSpec<TApi>>();
|
|
3016
|
+
for (const entry of entries) {
|
|
3017
|
+
const model = mapLiteLLMRichEntry(entry, options, runtimeBaseUrl);
|
|
3018
|
+
if (model) {
|
|
3019
|
+
deduped.set(model.id, model);
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
if (deduped.size === 0) {
|
|
3023
|
+
return null;
|
|
3024
|
+
}
|
|
3025
|
+
return Array.from(deduped.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
export async function fetchLiteLLMRichModels<TApi extends Api>(
|
|
3029
|
+
options: FetchLiteLLMRichModelsOptions<TApi>,
|
|
3030
|
+
): Promise<ModelSpec<TApi>[] | null> {
|
|
3031
|
+
const managementBaseUrl = normalizeLiteLLMManagementBaseUrl(options.baseUrl);
|
|
3032
|
+
const runtimeBaseUrl = normalizeLiteLLMRuntimeBaseUrl(options.baseUrl);
|
|
3033
|
+
if (!managementBaseUrl || !runtimeBaseUrl) {
|
|
3034
|
+
return null;
|
|
3035
|
+
}
|
|
3036
|
+
for (const endpoint of LITELLM_RICH_ENDPOINTS) {
|
|
3037
|
+
const models = await fetchLiteLLMRichEndpoint(endpoint, options, managementBaseUrl, runtimeBaseUrl);
|
|
3038
|
+
if (models) {
|
|
3039
|
+
return models;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
return null;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
2820
3045
|
export function litellmModelManagerOptions(
|
|
2821
3046
|
config?: LiteLLMModelManagerConfig,
|
|
2822
3047
|
): ModelManagerOptions<"openai-completions"> {
|
|
@@ -2824,21 +3049,32 @@ export function litellmModelManagerOptions(
|
|
|
2824
3049
|
const baseUrl = config?.baseUrl ?? Bun.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1";
|
|
2825
3050
|
return {
|
|
2826
3051
|
providerId: "litellm",
|
|
2827
|
-
cacheProviderId: `litellm:${Bun.hash(baseUrl).toString(36)}`,
|
|
2828
|
-
// litellm is a local-only proxy
|
|
2829
|
-
//
|
|
2830
|
-
//
|
|
2831
|
-
//
|
|
2832
|
-
// gateway providers (fireworks et al.) use — instead of a bundled map.
|
|
3052
|
+
cacheProviderId: `litellm:rich-v1:${Bun.hash(baseUrl).toString(36)}`,
|
|
3053
|
+
// litellm is a local-only proxy and is never bundled in models.json (that
|
|
3054
|
+
// would leak the machine's localhost catalog). Prefer the proxy's richer
|
|
3055
|
+
// management metadata, then fall back to /v1/models and enrich bare ids
|
|
3056
|
+
// against models.dev like the gateway providers (fireworks et al.) do.
|
|
2833
3057
|
fetchDynamicModels: async () => {
|
|
2834
3058
|
const modelsDevReferences = await loadModelsDevReferences<"openai-completions">(config?.fetch);
|
|
3059
|
+
const resolveReference = (id: string) => modelsDevReferences.get(id);
|
|
3060
|
+
const richModels = await fetchLiteLLMRichModels({
|
|
3061
|
+
api: "openai-completions",
|
|
3062
|
+
provider: "litellm",
|
|
3063
|
+
baseUrl,
|
|
3064
|
+
apiKey,
|
|
3065
|
+
fetch: config?.fetch,
|
|
3066
|
+
referenceResolver: resolveReference,
|
|
3067
|
+
signal: AbortSignal.timeout(10_000),
|
|
3068
|
+
});
|
|
3069
|
+
if (richModels && richModels.length > 0) {
|
|
3070
|
+
return richModels;
|
|
3071
|
+
}
|
|
2835
3072
|
return fetchOpenAICompatibleModels({
|
|
2836
3073
|
api: "openai-completions",
|
|
2837
3074
|
provider: "litellm",
|
|
2838
3075
|
baseUrl,
|
|
2839
3076
|
apiKey,
|
|
2840
|
-
mapModel: (entry, defaults) =>
|
|
2841
|
-
mapWithBundledReference(entry, defaults, modelsDevReferences.get(defaults.id)),
|
|
3077
|
+
mapModel: (entry, defaults) => mapWithBundledReference(entry, defaults, resolveReference(defaults.id)),
|
|
2842
3078
|
fetch: config?.fetch,
|
|
2843
3079
|
});
|
|
2844
3080
|
},
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { once } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { fetchCodexModels } from "../discovery/codex";
|
|
3
3
|
import type { DevinModelDiscoveryOptions } from "../discovery/devin";
|
|
4
|
+
import { buildGitLabDuoWorkflowFallbackModel, fetchGitLabDuoWorkflowModels } from "../discovery/gitlab-duo-workflow";
|
|
4
5
|
import type { ModelManagerOptions } from "../model-manager";
|
|
6
|
+
import type { FetchImpl } from "../types";
|
|
5
7
|
|
|
6
8
|
// ---------------------------------------------------------------------------
|
|
7
9
|
// OpenAI Codex
|
|
@@ -58,6 +60,68 @@ export function cursorModelManagerOptions(config: CursorModelManagerConfig = {})
|
|
|
58
60
|
const cursorDiscovery = once(() => import("../discovery/cursor"));
|
|
59
61
|
|
|
60
62
|
// ---------------------------------------------------------------------------
|
|
63
|
+
// GitLab Duo Workflow
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export interface GitLabDuoWorkflowModelManagerConfig {
|
|
67
|
+
apiKey?: string;
|
|
68
|
+
baseUrl?: string;
|
|
69
|
+
fetch?: FetchImpl;
|
|
70
|
+
namespaceId?: string;
|
|
71
|
+
projectId?: string;
|
|
72
|
+
cwd?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function gitLabDuoWorkflowModelManagerOptions(
|
|
76
|
+
config: GitLabDuoWorkflowModelManagerConfig = {},
|
|
77
|
+
): ModelManagerOptions<"gitlab-duo-agent"> {
|
|
78
|
+
const apiKey = config.apiKey;
|
|
79
|
+
return {
|
|
80
|
+
providerId: "gitlab-duo-agent",
|
|
81
|
+
// GitLab Duo discovery is credential- and namespace-specific
|
|
82
|
+
// (`aiChatAvailableModels(rootNamespaceId:)` also surfaces namespace-pinned
|
|
83
|
+
// models), so the default provider-id cache namespace would let a second
|
|
84
|
+
// account/namespace load the first one's authoritative model list at startup
|
|
85
|
+
// and skip refetching. Partition the cache by a non-reversible fingerprint of
|
|
86
|
+
// the exact inputs `fetchGitLabDuoWorkflowModels` resolves the namespace from
|
|
87
|
+
// (credential + base URL + namespace/project config + the same env vars + the
|
|
88
|
+
// effective workspace cwd whose git remote drives auto-discovery). Built-in
|
|
89
|
+
// discovery only passes apiKey/baseUrl/fetch, so the cwd/env terms — not the
|
|
90
|
+
// empty config fields — are what actually separate workspace A from B here.
|
|
91
|
+
// Falls back to the bare provider id when no credential is present.
|
|
92
|
+
...(apiKey ? { cacheProviderId: gitLabDuoWorkflowModelCacheProviderId(apiKey, config) } : undefined),
|
|
93
|
+
dynamicModelsAuthoritative: true,
|
|
94
|
+
staticModels: [
|
|
95
|
+
buildGitLabDuoWorkflowFallbackModel("claude_sonnet_4_6_vertex", "Claude Sonnet 4.6 - Vertex", config.baseUrl),
|
|
96
|
+
],
|
|
97
|
+
...(apiKey
|
|
98
|
+
? {
|
|
99
|
+
fetchDynamicModels: async () =>
|
|
100
|
+
fetchGitLabDuoWorkflowModels({
|
|
101
|
+
apiKey,
|
|
102
|
+
baseUrl: config.baseUrl,
|
|
103
|
+
fetch: config.fetch,
|
|
104
|
+
namespaceId: config.namespaceId,
|
|
105
|
+
projectId: config.projectId,
|
|
106
|
+
cwd: config.cwd,
|
|
107
|
+
}),
|
|
108
|
+
}
|
|
109
|
+
: undefined),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function gitLabDuoWorkflowModelCacheProviderId(apiKey: string, config: GitLabDuoWorkflowModelManagerConfig): string {
|
|
114
|
+
// Mirror the exact inputs `discoverGitLabDuoWorkflowNamespace` keys off: explicit
|
|
115
|
+
// namespace/project config OR the same env vars, then the git remote at the
|
|
116
|
+
// effective cwd. Built-in discovery leaves the config fields empty, so the env +
|
|
117
|
+
// resolved cwd terms are what actually distinguish two workspaces sharing a token.
|
|
118
|
+
const namespaceId = config.namespaceId ?? Bun.env.GITLAB_DUO_NAMESPACE_ID ?? "";
|
|
119
|
+
const projectId = config.projectId ?? Bun.env.GITLAB_DUO_PROJECT_ID ?? Bun.env.GITLAB_DUO_PROJECT_PATH ?? "";
|
|
120
|
+
const cwd = config.cwd ?? process.cwd();
|
|
121
|
+
const scope = [config.baseUrl ?? "", namespaceId, projectId, cwd].join("\u0000");
|
|
122
|
+
return `gitlab-duo-agent:${Bun.hash(`${apiKey}\u0000${scope}`).toString(36)}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
61
125
|
// Devin (Codeium Cascade)
|
|
62
126
|
// ---------------------------------------------------------------------------
|
|
63
127
|
|
|
@@ -84,7 +148,6 @@ export function devinModelManagerOptions(config: DevinModelManagerConfig = {}):
|
|
|
84
148
|
}
|
|
85
149
|
|
|
86
150
|
const devinDiscovery = once(() => import("../discovery/devin"));
|
|
87
|
-
|
|
88
151
|
// ---------------------------------------------------------------------------
|
|
89
152
|
// Zai
|
|
90
153
|
// ---------------------------------------------------------------------------
|
package/src/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type KnownApi =
|
|
|
15
15
|
| "google-vertex"
|
|
16
16
|
| "ollama-chat"
|
|
17
17
|
| "cursor-agent"
|
|
18
|
+
| "gitlab-duo-agent"
|
|
18
19
|
| "devin-agent";
|
|
19
20
|
export type Api = KnownApi | (string & {});
|
|
20
21
|
|
|
@@ -268,6 +269,14 @@ export interface OpenAICompat {
|
|
|
268
269
|
* to provider-default auto selection. Default: true.
|
|
269
270
|
*/
|
|
270
271
|
supportsForcedToolChoice?: boolean;
|
|
272
|
+
/**
|
|
273
|
+
* Whether the chat-completions endpoint accepts the object form that pins one
|
|
274
|
+
* named function (`{ type: "function", function: { name } }`). Some
|
|
275
|
+
* OpenAI-compatible hosts such as llama.cpp only accept string
|
|
276
|
+
* `tool_choice` values; request builders downgrade a named force to
|
|
277
|
+
* `"required"` when this is false. Default: true.
|
|
278
|
+
*/
|
|
279
|
+
supportsNamedToolChoice?: boolean;
|
|
271
280
|
/**
|
|
272
281
|
* Drop reasoning fields (`reasoning_effort`, OpenRouter `reasoning`) for
|
|
273
282
|
* the request when `tool_choice` forces a tool call. Mirrors the Anthropic
|
|
@@ -465,6 +474,7 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
465
474
|
disableReasoningOnToolChoice: boolean;
|
|
466
475
|
supportsToolChoice: boolean;
|
|
467
476
|
supportsForcedToolChoice: boolean;
|
|
477
|
+
supportsNamedToolChoice: boolean;
|
|
468
478
|
reasoningContentField?: OpenAICompat["reasoningContentField"];
|
|
469
479
|
requiresReasoningContentForToolCalls: boolean;
|
|
470
480
|
requiresReasoningContentForAllAssistantTurns: boolean;
|
|
@@ -516,6 +526,7 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
|
|
|
516
526
|
| "disableReasoningOnToolChoice"
|
|
517
527
|
| "supportsToolChoice"
|
|
518
528
|
| "supportsForcedToolChoice"
|
|
529
|
+
| "supportsNamedToolChoice"
|
|
519
530
|
| "reasoningContentField"
|
|
520
531
|
| "requiresReasoningContentForToolCalls"
|
|
521
532
|
| "requiresReasoningContentForAllAssistantTurns"
|
|
@@ -642,6 +653,18 @@ export type CompatOf<TApi extends Api> = TApi extends "openrouter"
|
|
|
642
653
|
? ResolvedDevinCompat
|
|
643
654
|
: undefined;
|
|
644
655
|
|
|
656
|
+
/** Provider-native compaction endpoint configuration for one model. */
|
|
657
|
+
export interface RemoteCompactionConfig<TApi extends Api = Api> {
|
|
658
|
+
/** Enables provider-native compaction for providers not enabled by built-in policy. */
|
|
659
|
+
enabled?: boolean;
|
|
660
|
+
/** Adapter family used by the configured compaction endpoint. */
|
|
661
|
+
api?: TApi;
|
|
662
|
+
/** Absolute compact endpoint URL; when omitted, the adapter derives it from the model base URL. */
|
|
663
|
+
endpoint?: string;
|
|
664
|
+
/** Model id sent to the compaction endpoint when it differs from the active model id. */
|
|
665
|
+
model?: string;
|
|
666
|
+
}
|
|
667
|
+
|
|
645
668
|
// Model interface for the unified model system
|
|
646
669
|
export interface Model<TApi extends Api = Api> {
|
|
647
670
|
id: string;
|
|
@@ -672,6 +695,8 @@ export interface Model<TApi extends Api = Api> {
|
|
|
672
695
|
* reports that native tool calling is unsupported.
|
|
673
696
|
*/
|
|
674
697
|
supportsTools?: boolean;
|
|
698
|
+
/** GitLab Duo Workflow root namespace selected during catalog discovery. */
|
|
699
|
+
gitlabDuoWorkflowRootNamespaceId?: string;
|
|
675
700
|
cost: {
|
|
676
701
|
input: number; // $/million tokens
|
|
677
702
|
output: number; // $/million tokens
|
|
@@ -714,6 +739,10 @@ export interface Model<TApi extends Api = Api> {
|
|
|
714
739
|
preferWebsockets?: boolean;
|
|
715
740
|
/** Preferred model to switch to when context promotion is triggered (model id or provider/id). */
|
|
716
741
|
contextPromotionTarget?: string;
|
|
742
|
+
/** Preferred model to use only for compaction (model id or provider/id); the active session model is unchanged. */
|
|
743
|
+
compactionModel?: string;
|
|
744
|
+
/** Provider-native compaction endpoint configuration. */
|
|
745
|
+
remoteCompaction?: RemoteCompactionConfig<TApi>;
|
|
717
746
|
/** Provider-assigned priority value (lower = higher priority). */
|
|
718
747
|
priority?: number;
|
|
719
748
|
/** Canonical thinking capability metadata for this model. */
|