@@ -42,7 +42,28 @@ _models_catalog_load() {
4242 printf ' %s' " $_MODELS_CATALOG_CACHE "
4343}
4444
45- # Resolve model alias to actual model ID
45+ # Parse "provider/model" reference format.
46+ # If input contains "/", split into provider and model.
47+ # Otherwise resolve alias first, then look up provider from catalog.
48+ # Sets two variables: _PARSED_PROVIDER and _PARSED_MODEL
49+ _parse_model_ref () {
50+ local raw=" $1 "
51+ _PARSED_PROVIDER=" "
52+ _PARSED_MODEL=" "
53+
54+ # Resolve alias first
55+ local resolved
56+ resolved=" $( _model_resolve_alias " $raw " ) "
57+
58+ if [[ " $resolved " == * " /" * ]]; then
59+ _PARSED_PROVIDER=" ${resolved%%/* } "
60+ _PARSED_MODEL=" ${resolved#*/ } "
61+ else
62+ _PARSED_MODEL=" $resolved "
63+ fi
64+ }
65+
66+ # Resolve model alias to actual model ref (may include provider/ prefix)
4667_model_resolve_alias () {
4768 local model=" $1 "
4869 local catalog
@@ -56,21 +77,48 @@ _model_resolve_alias() {
5677 fi
5778}
5879
80+ # Look up which provider owns a model ID by searching all providers
5981_model_provider () {
6082 local model=" $1 "
6183 local catalog
6284 catalog=" $( _models_catalog_load) "
6385 local provider
64- provider=" $( printf ' %s' " $catalog " | jq -r --arg m " $model " ' .models[$m].provider // empty' 2> /dev/null) "
86+ provider=" $( printf ' %s' " $catalog " | jq -r --arg m " $model " '
87+ .providers | to_entries[] | select(.value.models[]?.id == $m) | .key
88+ ' 2> /dev/null | head -1) "
6589 printf ' %s' " $provider "
6690}
6791
68- _model_max_tokens () {
92+ # Get a field from a model definition across all providers
93+ _model_get_field () {
6994 local model=" $1 "
95+ local field=" $2 "
7096 local catalog
7197 catalog=" $( _models_catalog_load) "
98+ local val
99+ val=" $( printf ' %s' " $catalog " | jq -r --arg m " $model " --arg f " $field " '
100+ [.providers[].models[] | select(.id == $m)] | .[0][$f] // empty
101+ ' 2> /dev/null) "
102+ printf ' %s' " $val "
103+ }
104+
105+ # Get a compat flag from a model definition
106+ _model_get_compat_field () {
107+ local model=" $1 "
108+ local field=" $2 "
109+ local catalog
110+ catalog=" $( _models_catalog_load) "
111+ local val
112+ val=" $( printf ' %s' " $catalog " | jq -r --arg m " $model " --arg f " $field " '
113+ [.providers[].models[] | select(.id == $m)] | .[0].compat[$f] // empty
114+ ' 2> /dev/null) "
115+ printf ' %s' " $val "
116+ }
117+
118+ _model_max_tokens () {
119+ local model=" $1 "
72120 local tokens
73- tokens=" $( printf ' %s ' " $catalog " | jq -r --arg m " $ model" ' .models[$m]. max_tokens // empty ' 2> /dev/null ) "
121+ tokens=" $( _model_get_field " $ model" " max_tokens" ) "
74122 if [[ -z " $tokens " ]]; then
75123 printf ' 4096'
76124 else
@@ -80,10 +128,8 @@ _model_max_tokens() {
80128
81129_model_context_window () {
82130 local model=" $1 "
83- local catalog
84- catalog=" $( _models_catalog_load) "
85131 local window
86- window=" $( printf ' %s ' " $catalog " | jq -r --arg m " $ model" ' .models[$m]. context_window // empty ' 2> /dev/null ) "
132+ window=" $( _model_get_field " $ model" " context_window" ) "
87133 if [[ -z " $window " ]]; then
88134 printf ' 128000'
89135 else
@@ -102,17 +148,25 @@ agent_resolve_model() {
102148 local model
103149 model=" $( config_agent_get " $agent_id " " model" " " ) "
104150 if [[ -z " $model " ]]; then
105- model=" ${MODEL_ID:- claude-sonnet -4-20250514 } "
151+ model=" ${MODEL_ID:- claude-opus -4-6 } "
106152 fi
107153
108- # Resolve alias to actual model ID
109- model= " $( _model_resolve_alias " $model " ) "
110- printf ' %s' " $model "
154+ # Parse provider/model ref (resolves aliases internally)
155+ _parse_model_ref " $model "
156+ printf ' %s' " $_PARSED_MODEL "
111157}
112158
113159agent_resolve_provider () {
114160 local model=" $1 "
115161
162+ # If caller already set _PARSED_PROVIDER from _parse_model_ref, use it
163+ if [[ -n " ${_PARSED_PROVIDER:- } " ]]; then
164+ printf ' %s' " $_PARSED_PROVIDER "
165+ _PARSED_PROVIDER=" "
166+ return
167+ fi
168+
169+ # Look up provider from catalog
116170 local provider
117171 provider=" $( _model_provider " $model " ) "
118172 if [[ -n " $provider " ]]; then
@@ -122,14 +176,20 @@ agent_resolve_provider() {
122176
123177 # Infer provider from model name patterns
124178 case " $model " in
125- claude-* |claude3* ) printf ' anthropic' ; return ;;
126- gpt-* |o1* |o3* |o4* ) printf ' openai' ; return ;;
127- gemini-* ) printf ' google' ; return ;;
128- deepseek-* ) printf ' deepseek' ; return ;;
129- qwen-* |qwq-* ) printf ' qwen' ; return ;;
130- glm-* ) printf ' zhipu' ; return ;;
131- moonshot-* |kimi-* ) printf ' moonshot' ; return ;;
132- MiniMax-* |minimax-* |abab* ) printf ' minimax' ; return ;;
179+ claude-* ) printf ' anthropic' ; return ;;
180+ gpt-* |o1* |o3* |o4* ) printf ' openai' ; return ;;
181+ gemini-* ) printf ' google' ; return ;;
182+ deepseek-* ) printf ' deepseek' ; return ;;
183+ qwen-* |qwq-* ) printf ' qwen' ; return ;;
184+ glm-* ) printf ' zhipu' ; return ;;
185+ moonshot-* |kimi-* ) printf ' moonshot' ; return ;;
186+ MiniMax-* |minimax-* |abab* ) printf ' minimax' ; return ;;
187+ mimo-* ) printf ' xiaomi' ; return ;;
188+ ernie-* ) printf ' qianfan' ; return ;;
189+ nvidia/* ) printf ' nvidia' ; return ;;
190+ llama-* |meta/* ) printf ' groq' ; return ;;
191+ grok-* ) printf ' xai' ; return ;;
192+ mistral-* ) printf ' mistral' ; return ;;
133193 esac
134194
135195 # If OPENROUTER_API_KEY is set and model is unknown, assume openrouter
@@ -150,7 +210,6 @@ agent_resolve_api_key() {
150210 local catalog
151211 catalog=" $( _models_catalog_load) "
152212
153- # Look up the env var name for this provider from models.json
154213 local key_env
155214 key_env=" $( printf ' %s' " $catalog " | jq -r --arg p " $provider " \
156215 ' .providers[$p].api_key_env // empty' 2> /dev/null) "
@@ -159,10 +218,24 @@ agent_resolve_api_key() {
159218 log_fatal " Unknown provider: $provider (not found in models.json)"
160219 fi
161220
162- # Read the actual key value from the environment
163221 local key
164222 eval " key=\"\$ {${key_env} :-}\" "
165223
224+ # Backward compat: check legacy env var names
225+ if [[ -z " $key " ]]; then
226+ case " $provider " in
227+ google) key=" ${GOOGLE_API_KEY:- } " ;;
228+ zhipu) key=" ${ZHIPU_API_KEY:- } " ;;
229+ esac
230+ fi
231+
232+ # Ollama/vLLM: API key is optional for local providers
233+ if [[ -z " $key " ]]; then
234+ case " $provider " in
235+ ollama|vllm) key=" no-key-required" ; return 0 ;;
236+ esac
237+ fi
238+
166239 if [[ -z " $key " ]]; then
167240 log_fatal " ${key_env} is required for ${provider} provider"
168241 fi
@@ -176,24 +249,31 @@ _provider_api_url() {
176249 local catalog
177250 catalog=" $( _models_catalog_load) "
178251
179- # Check for env var override first
180- local url_env
181- url_env=" $( printf ' %s' " $catalog " | jq -r --arg p " $provider " \
182- ' .providers[$p].api_url_env // empty' 2> /dev/null) "
252+ local url_default
253+ url_default=" $( printf ' %s' " $catalog " | jq -r --arg p " $provider " \
254+ ' .providers[$p].base_url // empty' 2> /dev/null) "
255+
256+ # Check env var override: {PROVIDER}_BASE_URL
257+ local env_key
258+ case " $provider " in
259+ anthropic) env_key=" ANTHROPIC_BASE_URL" ;;
260+ openai) env_key=" OPENAI_BASE_URL" ;;
261+ google) env_key=" GOOGLE_AI_BASE_URL" ;;
262+ openrouter) env_key=" OPENROUTER_BASE_URL" ;;
263+ ollama) env_key=" OLLAMA_BASE_URL" ;;
264+ vllm) env_key=" VLLM_BASE_URL" ;;
265+ * ) env_key=" " ;;
266+ esac
183267
184- if [[ -n " $url_env " ]]; then
268+ if [[ -n " $env_key " ]]; then
185269 local url_override
186- eval " url_override=\"\$ {${url_env } :-}\" "
270+ eval " url_override=\"\$ {${env_key } :-}\" "
187271 if [[ -n " $url_override " ]]; then
188272 printf ' %s' " $url_override "
189273 return
190274 fi
191275 fi
192276
193- # Fall back to default URL
194- local url_default
195- url_default=" $( printf ' %s' " $catalog " | jq -r --arg p " $provider " \
196- ' .providers[$p].api_url_default // empty' 2> /dev/null) "
197277 printf ' %s' " $url_default "
198278}
199279
@@ -205,7 +285,7 @@ _provider_api_format() {
205285 catalog=" $( _models_catalog_load) "
206286 local fmt
207287 fmt=" $( printf ' %s' " $catalog " | jq -r --arg p " $provider " \
208- ' .providers[$p].api_format // empty' 2> /dev/null) "
288+ ' .providers[$p].api // empty' 2> /dev/null) "
209289
210290 if [[ -z " $fmt " ]]; then
211291 printf ' openai'
@@ -214,6 +294,15 @@ _provider_api_format() {
214294 fi
215295}
216296
297+ # Get the API version header value for a provider (e.g. anthropic)
298+ _provider_api_version () {
299+ local provider=" $1 "
300+ local catalog
301+ catalog=" $( _models_catalog_load) "
302+ printf ' %s' " $( printf ' %s' " $catalog " | jq -r --arg p " $provider " \
303+ ' .providers[$p].api_version // empty' 2> /dev/null) "
304+ }
305+
217306# Resolve the next fallback model from the configured fallback chain.
218307# Returns the fallback model name, or empty if none available.
219308agent_resolve_fallback_model () {
@@ -817,6 +906,14 @@ agent_call_openai() {
817906
818907 local api_url=" ${api_base} /v1/chat/completions"
819908
909+ # Check if model uses max_completion_tokens (o-series models)
910+ local max_tokens_field=" max_tokens"
911+ local compat_field
912+ compat_field=" $( _model_get_compat_field " $model " " max_tokens_field" ) "
913+ if [[ -n " $compat_field " ]]; then
914+ max_tokens_field=" $compat_field "
915+ fi
916+
820917 local oai_messages
821918 oai_messages=" $( printf ' %s' " $messages " | jq --arg sys " $system_prompt " \
822919 ' [{role: "system", content: $sys}] + .' ) "
@@ -841,10 +938,11 @@ agent_call_openai() {
841938 --argjson max_tokens " $max_tokens " \
842939 --argjson temp " $temperature " \
843940 --argjson tools " $oai_tools " \
941+ --arg mtf " $max_tokens_field " \
844942 ' {
845943 model: $model,
846944 messages: $messages,
847- max_tokens : $max_tokens,
945+ ($mtf) : $max_tokens,
848946 temperature: $temp,
849947 tools: $tools
850948 }' ) "
@@ -854,10 +952,11 @@ agent_call_openai() {
854952 --argjson messages " $oai_messages " \
855953 --argjson max_tokens " $max_tokens " \
856954 --argjson temp " $temperature " \
955+ --arg mtf " $max_tokens_field " \
857956 ' {
858957 model: $model,
859958 messages: $messages,
860- max_tokens : $max_tokens,
959+ ($mtf) : $max_tokens,
861960 temperature: $temp
862961 }' ) "
863962 fi
0 commit comments