@open-mercato/shared 0.6.1-develop.3191.1.2511ae315f → 0.6.1-develop.3198.1.aa979623f0
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/dist/lib/ai/opencode-provider.js +19 -1
- package/dist/lib/ai/opencode-provider.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/ai/__tests__/opencode-provider.test.ts +64 -4
- package/src/lib/ai/opencode-provider.ts +40 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const OPEN_CODE_PROVIDER_IDS = ["anthropic", "openai", "google"];
|
|
2
|
+
const DEFAULT_AI_PROVIDER_ID = "openai";
|
|
2
3
|
const OPEN_CODE_PROVIDERS = {
|
|
3
4
|
anthropic: {
|
|
4
5
|
id: "anthropic",
|
|
@@ -10,7 +11,7 @@ const OPEN_CODE_PROVIDERS = {
|
|
|
10
11
|
id: "openai",
|
|
11
12
|
name: "OpenAI",
|
|
12
13
|
envKeys: ["OPENAI_API_KEY", "OPENCODE_OPENAI_API_KEY"],
|
|
13
|
-
defaultModel: "gpt-
|
|
14
|
+
defaultModel: "gpt-5-mini"
|
|
14
15
|
},
|
|
15
16
|
google: {
|
|
16
17
|
id: "google",
|
|
@@ -19,6 +20,16 @@ const OPEN_CODE_PROVIDERS = {
|
|
|
19
20
|
defaultModel: "gemini-3-flash"
|
|
20
21
|
}
|
|
21
22
|
};
|
|
23
|
+
function resolveAiProviderIdFromEnv(env = process.env, fallback = DEFAULT_AI_PROVIDER_ID) {
|
|
24
|
+
const candidates = [env.OM_AI_PROVIDER, env.OPENCODE_PROVIDER];
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
const normalized = normalizeToken(candidate)?.toLowerCase();
|
|
27
|
+
if (normalized && isOpenCodeProviderId(normalized)) {
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
22
33
|
function normalizeToken(value) {
|
|
23
34
|
if (typeof value !== "string") return null;
|
|
24
35
|
const normalized = value.trim();
|
|
@@ -93,6 +104,7 @@ function isOpenCodeProviderConfigured(providerId, env = process.env) {
|
|
|
93
104
|
function resolveOpenCodeModel(providerId, options) {
|
|
94
105
|
const env = options?.env ?? process.env;
|
|
95
106
|
const overrideModel = normalizeToken(options?.overrideModel);
|
|
107
|
+
const omAiModel = normalizeToken(env.OM_AI_MODEL);
|
|
96
108
|
const opencodeModel = normalizeToken(env.OPENCODE_MODEL);
|
|
97
109
|
let source = "default";
|
|
98
110
|
let selectedModel = OPEN_CODE_PROVIDERS[providerId].defaultModel;
|
|
@@ -100,6 +112,10 @@ function resolveOpenCodeModel(providerId, options) {
|
|
|
100
112
|
selectedModel = opencodeModel;
|
|
101
113
|
source = "opencode_model";
|
|
102
114
|
}
|
|
115
|
+
if (omAiModel) {
|
|
116
|
+
selectedModel = omAiModel;
|
|
117
|
+
source = "om_ai_model";
|
|
118
|
+
}
|
|
103
119
|
if (overrideModel) {
|
|
104
120
|
selectedModel = overrideModel;
|
|
105
121
|
source = "override";
|
|
@@ -121,12 +137,14 @@ function resolveOpenCodeModel(providerId, options) {
|
|
|
121
137
|
};
|
|
122
138
|
}
|
|
123
139
|
export {
|
|
140
|
+
DEFAULT_AI_PROVIDER_ID,
|
|
124
141
|
OPEN_CODE_PROVIDERS,
|
|
125
142
|
OPEN_CODE_PROVIDER_IDS,
|
|
126
143
|
getOpenCodeProviderConfiguredEnvKey,
|
|
127
144
|
isOpenCodeProviderConfigured,
|
|
128
145
|
isOpenCodeProviderId,
|
|
129
146
|
requireOpenCodeProviderApiKey,
|
|
147
|
+
resolveAiProviderIdFromEnv,
|
|
130
148
|
resolveFirstConfiguredOpenCodeProvider,
|
|
131
149
|
resolveOpenCodeModel,
|
|
132
150
|
resolveOpenCodeProviderApiKey,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/ai/opencode-provider.ts"],
|
|
4
|
-
"sourcesContent": ["type EnvLookup = Record<string, string | undefined>\n\nexport type OpenCodeProviderId = 'anthropic' | 'openai' | 'google'\n\nexport type OpenCodeProviderDefinition = {\n id: OpenCodeProviderId\n name: string\n envKeys: readonly string[]\n defaultModel: string\n}\n\nexport const OPEN_CODE_PROVIDER_IDS = ['anthropic', 'openai', 'google'] as const\n\nexport const OPEN_CODE_PROVIDERS: Record<OpenCodeProviderId, OpenCodeProviderDefinition> = {\n anthropic: {\n id: 'anthropic',\n name: 'Anthropic',\n envKeys: ['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'],\n defaultModel: 'claude-haiku-4-5-20251001',\n },\n openai: {\n id: 'openai',\n name: 'OpenAI',\n envKeys: ['OPENAI_API_KEY', 'OPENCODE_OPENAI_API_KEY'],\n defaultModel: 'gpt-
|
|
5
|
-
"mappings": "AAWO,MAAM,yBAAyB,CAAC,aAAa,UAAU,QAAQ;
|
|
4
|
+
"sourcesContent": ["type EnvLookup = Record<string, string | undefined>\n\nexport type OpenCodeProviderId = 'anthropic' | 'openai' | 'google'\n\nexport type OpenCodeProviderDefinition = {\n id: OpenCodeProviderId\n name: string\n envKeys: readonly string[]\n defaultModel: string\n}\n\nexport const OPEN_CODE_PROVIDER_IDS = ['anthropic', 'openai', 'google'] as const\n\n/**\n * Default provider id used by the unified AI framework when neither\n * `OM_AI_PROVIDER` nor the legacy `OPENCODE_PROVIDER` is set. The default\n * targets OpenAI + `gpt-5-mini` (see {@link OPEN_CODE_PROVIDERS.openai}).\n */\nexport const DEFAULT_AI_PROVIDER_ID: OpenCodeProviderId = 'openai'\n\nexport const OPEN_CODE_PROVIDERS: Record<OpenCodeProviderId, OpenCodeProviderDefinition> = {\n anthropic: {\n id: 'anthropic',\n name: 'Anthropic',\n envKeys: ['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'],\n defaultModel: 'claude-haiku-4-5-20251001',\n },\n openai: {\n id: 'openai',\n name: 'OpenAI',\n envKeys: ['OPENAI_API_KEY', 'OPENCODE_OPENAI_API_KEY'],\n defaultModel: 'gpt-5-mini',\n },\n google: {\n id: 'google',\n name: 'Google',\n envKeys: ['GOOGLE_GENERATIVE_AI_API_KEY', 'OPENCODE_GOOGLE_API_KEY'],\n defaultModel: 'gemini-3-flash',\n },\n}\n\nexport type OpenCodeModelResolution = {\n modelId: string\n modelWithProvider: string\n source: 'override' | 'om_ai_model' | 'opencode_model' | 'default'\n}\n\n/**\n * Resolves the requested AI provider id from the unified env vars.\n *\n * Precedence (highest first):\n * 1. `OM_AI_PROVIDER` \u2014 new canonical variable for the unified AI module.\n * 2. `OPENCODE_PROVIDER` \u2014 legacy variable, kept for backward compatibility.\n * 3. {@link DEFAULT_AI_PROVIDER_ID} (currently `openai`).\n *\n * Unknown / empty values fall through to the next tier so a typo in\n * `OM_AI_PROVIDER` does not silently disable the legacy fallback.\n */\nexport function resolveAiProviderIdFromEnv(\n env: EnvLookup = process.env,\n fallback: OpenCodeProviderId = DEFAULT_AI_PROVIDER_ID,\n): OpenCodeProviderId {\n const candidates = [env.OM_AI_PROVIDER, env.OPENCODE_PROVIDER]\n for (const candidate of candidates) {\n const normalized = normalizeToken(candidate)?.toLowerCase()\n if (normalized && isOpenCodeProviderId(normalized)) {\n return normalized\n }\n }\n return fallback\n}\n\nfunction normalizeToken(value: string | null | undefined): string | null {\n if (typeof value !== 'string') return null\n const normalized = value.trim()\n return normalized.length > 0 ? normalized : null\n}\n\nfunction parseModelToken(token: string): { providerPrefix: string | null; modelId: string } {\n const slashIndex = token.indexOf('/')\n if (slashIndex <= 0) {\n return {\n providerPrefix: null,\n modelId: token,\n }\n }\n\n return {\n providerPrefix: token.slice(0, slashIndex).trim().toLowerCase(),\n modelId: token.slice(slashIndex + 1).trim(),\n }\n}\n\nexport function isOpenCodeProviderId(value: string): value is OpenCodeProviderId {\n return (OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(value)\n}\n\nexport function resolveOpenCodeProviderId(\n providerId: string | null | undefined,\n fallback: OpenCodeProviderId = 'anthropic',\n): OpenCodeProviderId {\n const normalized = normalizeToken(providerId)?.toLowerCase()\n if (normalized && isOpenCodeProviderId(normalized)) {\n return normalized\n }\n\n return fallback\n}\n\nexport function resolveFirstConfiguredOpenCodeProvider(\n options?: {\n env?: EnvLookup\n order?: readonly OpenCodeProviderId[]\n },\n): OpenCodeProviderId | null {\n const env = options?.env ?? process.env\n const order = options?.order ?? OPEN_CODE_PROVIDER_IDS\n\n for (const providerId of order) {\n if (isOpenCodeProviderConfigured(providerId, env)) {\n return providerId\n }\n }\n\n return null\n}\n\nexport function resolveOpenCodeProviderApiKey(\n providerId: OpenCodeProviderId,\n env: EnvLookup = process.env,\n): string | null {\n const provider = OPEN_CODE_PROVIDERS[providerId]\n for (const key of provider.envKeys) {\n const value = normalizeToken(env[key])\n if (value) {\n return value\n }\n }\n\n return null\n}\n\nexport function requireOpenCodeProviderApiKey(\n providerId: OpenCodeProviderId,\n env: EnvLookup = process.env,\n): string {\n const apiKey = resolveOpenCodeProviderApiKey(providerId, env)\n if (apiKey) {\n return apiKey\n }\n\n const provider = OPEN_CODE_PROVIDERS[providerId]\n const envKeysHint = provider.envKeys.join(' or ')\n throw new Error(\n `Missing API key for provider \"${providerId}\". Set ${envKeysHint} in your .env file.`,\n )\n}\n\nexport function getOpenCodeProviderConfiguredEnvKey(\n providerId: OpenCodeProviderId,\n env: EnvLookup = process.env,\n): string {\n const provider = OPEN_CODE_PROVIDERS[providerId]\n for (const key of provider.envKeys) {\n if (normalizeToken(env[key])) {\n return key\n }\n }\n\n return provider.envKeys[0]\n}\n\nexport function isOpenCodeProviderConfigured(\n providerId: OpenCodeProviderId,\n env: EnvLookup = process.env,\n): boolean {\n return resolveOpenCodeProviderApiKey(providerId, env) !== null\n}\n\nexport function resolveOpenCodeModel(\n providerId: OpenCodeProviderId,\n options?: {\n overrideModel?: string | null | undefined\n env?: EnvLookup\n },\n): OpenCodeModelResolution {\n const env = options?.env ?? process.env\n const overrideModel = normalizeToken(options?.overrideModel)\n const omAiModel = normalizeToken(env.OM_AI_MODEL)\n const opencodeModel = normalizeToken(env.OPENCODE_MODEL)\n\n let source: OpenCodeModelResolution['source'] = 'default'\n let selectedModel = OPEN_CODE_PROVIDERS[providerId].defaultModel\n\n if (opencodeModel) {\n selectedModel = opencodeModel\n source = 'opencode_model'\n }\n\n if (omAiModel) {\n selectedModel = omAiModel\n source = 'om_ai_model'\n }\n\n if (overrideModel) {\n selectedModel = overrideModel\n source = 'override'\n }\n\n const parsed = parseModelToken(selectedModel)\n if (parsed.providerPrefix && parsed.providerPrefix !== providerId) {\n throw new Error(\n `Model \"${selectedModel}\" does not match configured provider \"${providerId}\"`,\n )\n }\n\n const modelId = normalizeToken(parsed.modelId)\n if (!modelId) {\n throw new Error(`Model \"${selectedModel}\" is invalid`)\n }\n\n return {\n modelId,\n modelWithProvider: `${providerId}/${modelId}`,\n source,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.1-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.1-develop.3198.1.aa979623f0'\nexport const appVersion = APP_VERSION\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.1-develop.
|
|
3
|
+
"version": "0.6.1-develop.3198.1.aa979623f0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"@mikro-orm/core": "^7.0.14",
|
|
93
93
|
"@mikro-orm/decorators": "^7.0.14",
|
|
94
94
|
"@mikro-orm/postgresql": "^7.0.14",
|
|
95
|
-
"@open-mercato/cache": "0.6.1-develop.
|
|
95
|
+
"@open-mercato/cache": "0.6.1-develop.3198.1.aa979623f0",
|
|
96
96
|
"dotenv": "^17.4.2",
|
|
97
97
|
"rate-limiter-flexible": "^11.0.1",
|
|
98
98
|
"reflect-metadata": "^0.2.2",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
resolveAiProviderIdFromEnv,
|
|
2
3
|
resolveOpenCodeProviderId,
|
|
3
4
|
resolveFirstConfiguredOpenCodeProvider,
|
|
4
5
|
resolveOpenCodeModel,
|
|
@@ -47,8 +48,8 @@ describe('opencode provider helpers', () => {
|
|
|
47
48
|
it('resolves default model when no overrides are set', () => {
|
|
48
49
|
const model = resolveOpenCodeModel('openai', { env: {} })
|
|
49
50
|
expect(model).toEqual({
|
|
50
|
-
modelId: 'gpt-
|
|
51
|
-
modelWithProvider: 'openai/gpt-
|
|
51
|
+
modelId: 'gpt-5-mini',
|
|
52
|
+
modelWithProvider: 'openai/gpt-5-mini',
|
|
52
53
|
source: 'default',
|
|
53
54
|
})
|
|
54
55
|
})
|
|
@@ -64,11 +65,36 @@ describe('opencode provider helpers', () => {
|
|
|
64
65
|
})
|
|
65
66
|
})
|
|
66
67
|
|
|
67
|
-
it('prefers
|
|
68
|
+
it('prefers OM_AI_MODEL over the legacy OPENCODE_MODEL', () => {
|
|
69
|
+
const model = resolveOpenCodeModel('openai', {
|
|
70
|
+
env: {
|
|
71
|
+
OM_AI_MODEL: 'openai/gpt-5-mini',
|
|
72
|
+
OPENCODE_MODEL: 'openai/gpt-4.1-mini',
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
expect(model).toEqual({
|
|
76
|
+
modelId: 'gpt-5-mini',
|
|
77
|
+
modelWithProvider: 'openai/gpt-5-mini',
|
|
78
|
+
source: 'om_ai_model',
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('falls back to OPENCODE_MODEL when OM_AI_MODEL is unset', () => {
|
|
68
83
|
const model = resolveOpenCodeModel('openai', {
|
|
69
|
-
overrideModel: 'gpt-4.1',
|
|
70
84
|
env: { OPENCODE_MODEL: 'openai/gpt-4.1-mini' },
|
|
71
85
|
})
|
|
86
|
+
expect(model.source).toBe('opencode_model')
|
|
87
|
+
expect(model.modelId).toBe('gpt-4.1-mini')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('prefers explicit override model over OM_AI_MODEL and OPENCODE_MODEL', () => {
|
|
91
|
+
const model = resolveOpenCodeModel('openai', {
|
|
92
|
+
overrideModel: 'gpt-4.1',
|
|
93
|
+
env: {
|
|
94
|
+
OM_AI_MODEL: 'openai/gpt-5-mini',
|
|
95
|
+
OPENCODE_MODEL: 'openai/gpt-4.1-mini',
|
|
96
|
+
},
|
|
97
|
+
})
|
|
72
98
|
expect(model).toEqual({
|
|
73
99
|
modelId: 'gpt-4.1',
|
|
74
100
|
modelWithProvider: 'openai/gpt-4.1',
|
|
@@ -76,6 +102,40 @@ describe('opencode provider helpers', () => {
|
|
|
76
102
|
})
|
|
77
103
|
})
|
|
78
104
|
|
|
105
|
+
describe('resolveAiProviderIdFromEnv', () => {
|
|
106
|
+
it('returns OM_AI_PROVIDER when set', () => {
|
|
107
|
+
expect(resolveAiProviderIdFromEnv({ OM_AI_PROVIDER: 'anthropic' })).toBe('anthropic')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('falls back to OPENCODE_PROVIDER when OM_AI_PROVIDER is unset', () => {
|
|
111
|
+
expect(
|
|
112
|
+
resolveAiProviderIdFromEnv({ OPENCODE_PROVIDER: 'google' }),
|
|
113
|
+
).toBe('google')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('prefers OM_AI_PROVIDER over OPENCODE_PROVIDER', () => {
|
|
117
|
+
expect(
|
|
118
|
+
resolveAiProviderIdFromEnv({
|
|
119
|
+
OM_AI_PROVIDER: 'openai',
|
|
120
|
+
OPENCODE_PROVIDER: 'anthropic',
|
|
121
|
+
}),
|
|
122
|
+
).toBe('openai')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('skips unknown values and falls through to the legacy var', () => {
|
|
126
|
+
expect(
|
|
127
|
+
resolveAiProviderIdFromEnv({
|
|
128
|
+
OM_AI_PROVIDER: 'pirate',
|
|
129
|
+
OPENCODE_PROVIDER: 'google',
|
|
130
|
+
}),
|
|
131
|
+
).toBe('google')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('defaults to openai when neither var is set', () => {
|
|
135
|
+
expect(resolveAiProviderIdFromEnv({})).toBe('openai')
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
79
139
|
it('throws when model provider prefix conflicts with selected provider', () => {
|
|
80
140
|
expect(() =>
|
|
81
141
|
resolveOpenCodeModel('openai', {
|
|
@@ -11,6 +11,13 @@ export type OpenCodeProviderDefinition = {
|
|
|
11
11
|
|
|
12
12
|
export const OPEN_CODE_PROVIDER_IDS = ['anthropic', 'openai', 'google'] as const
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Default provider id used by the unified AI framework when neither
|
|
16
|
+
* `OM_AI_PROVIDER` nor the legacy `OPENCODE_PROVIDER` is set. The default
|
|
17
|
+
* targets OpenAI + `gpt-5-mini` (see {@link OPEN_CODE_PROVIDERS.openai}).
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_AI_PROVIDER_ID: OpenCodeProviderId = 'openai'
|
|
20
|
+
|
|
14
21
|
export const OPEN_CODE_PROVIDERS: Record<OpenCodeProviderId, OpenCodeProviderDefinition> = {
|
|
15
22
|
anthropic: {
|
|
16
23
|
id: 'anthropic',
|
|
@@ -22,7 +29,7 @@ export const OPEN_CODE_PROVIDERS: Record<OpenCodeProviderId, OpenCodeProviderDef
|
|
|
22
29
|
id: 'openai',
|
|
23
30
|
name: 'OpenAI',
|
|
24
31
|
envKeys: ['OPENAI_API_KEY', 'OPENCODE_OPENAI_API_KEY'],
|
|
25
|
-
defaultModel: 'gpt-
|
|
32
|
+
defaultModel: 'gpt-5-mini',
|
|
26
33
|
},
|
|
27
34
|
google: {
|
|
28
35
|
id: 'google',
|
|
@@ -35,7 +42,32 @@ export const OPEN_CODE_PROVIDERS: Record<OpenCodeProviderId, OpenCodeProviderDef
|
|
|
35
42
|
export type OpenCodeModelResolution = {
|
|
36
43
|
modelId: string
|
|
37
44
|
modelWithProvider: string
|
|
38
|
-
source: 'override' | 'opencode_model' | 'default'
|
|
45
|
+
source: 'override' | 'om_ai_model' | 'opencode_model' | 'default'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the requested AI provider id from the unified env vars.
|
|
50
|
+
*
|
|
51
|
+
* Precedence (highest first):
|
|
52
|
+
* 1. `OM_AI_PROVIDER` — new canonical variable for the unified AI module.
|
|
53
|
+
* 2. `OPENCODE_PROVIDER` — legacy variable, kept for backward compatibility.
|
|
54
|
+
* 3. {@link DEFAULT_AI_PROVIDER_ID} (currently `openai`).
|
|
55
|
+
*
|
|
56
|
+
* Unknown / empty values fall through to the next tier so a typo in
|
|
57
|
+
* `OM_AI_PROVIDER` does not silently disable the legacy fallback.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveAiProviderIdFromEnv(
|
|
60
|
+
env: EnvLookup = process.env,
|
|
61
|
+
fallback: OpenCodeProviderId = DEFAULT_AI_PROVIDER_ID,
|
|
62
|
+
): OpenCodeProviderId {
|
|
63
|
+
const candidates = [env.OM_AI_PROVIDER, env.OPENCODE_PROVIDER]
|
|
64
|
+
for (const candidate of candidates) {
|
|
65
|
+
const normalized = normalizeToken(candidate)?.toLowerCase()
|
|
66
|
+
if (normalized && isOpenCodeProviderId(normalized)) {
|
|
67
|
+
return normalized
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return fallback
|
|
39
71
|
}
|
|
40
72
|
|
|
41
73
|
function normalizeToken(value: string | null | undefined): string | null {
|
|
@@ -154,6 +186,7 @@ export function resolveOpenCodeModel(
|
|
|
154
186
|
): OpenCodeModelResolution {
|
|
155
187
|
const env = options?.env ?? process.env
|
|
156
188
|
const overrideModel = normalizeToken(options?.overrideModel)
|
|
189
|
+
const omAiModel = normalizeToken(env.OM_AI_MODEL)
|
|
157
190
|
const opencodeModel = normalizeToken(env.OPENCODE_MODEL)
|
|
158
191
|
|
|
159
192
|
let source: OpenCodeModelResolution['source'] = 'default'
|
|
@@ -164,6 +197,11 @@ export function resolveOpenCodeModel(
|
|
|
164
197
|
source = 'opencode_model'
|
|
165
198
|
}
|
|
166
199
|
|
|
200
|
+
if (omAiModel) {
|
|
201
|
+
selectedModel = omAiModel
|
|
202
|
+
source = 'om_ai_model'
|
|
203
|
+
}
|
|
204
|
+
|
|
167
205
|
if (overrideModel) {
|
|
168
206
|
selectedModel = overrideModel
|
|
169
207
|
source = 'override'
|