mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-08-26 16:55:43 +00:00
Restore configurable Vision Model call limits
Expose timeout and max-token controls only in the visible Vision sidecar advanced settings, defaulting to 300 seconds and 2000 tokens, and pass them through the model builder. Point prompt customization to Agent Editor and promote legacy kwargs into the dedicated preset fields.
This commit is contained in:
parent
519f2c2a62
commit
906eee4da7
6 changed files with 188 additions and 11 deletions
|
|
@ -26,6 +26,7 @@
|
|||
- The optional `vision` slot is strictly per preset and never inherited from `Default`; an empty slot disables the separate Vision Model for that preset.
|
||||
- Main native vision wins by default. A configured Vision Model handles `vision_load` when Main lacks vision, or when that preset explicitly enables `override_main`.
|
||||
- Keep the optional Vision provider/model selector inside the Main Model card and flush with Main's field alignment, without a nested left inset. Show it only while Main vision is disabled or `override_main` is enabled; do not render a standalone Vision Model card.
|
||||
- Keep Vision timeout and maximum-output-token controls, plus the Agent Editor prompt-customization note, inside the visible Vision sidecar's Advanced Settings only. The Vision call limits belong to the preset/model builder, not to `vision_load` call-site constants.
|
||||
- Show `Use separate Vision Model` immediately below `Supports Vision` while Main vision is enabled, not inside Advanced Settings; describe the disabled state as using Main's native vision.
|
||||
- In model overviews, render the effective Vision Model as a text-only `Vision override / Provider / Model` child aligned with Main's provider column, not as an icon-bearing peer row.
|
||||
- Changing a model provider in the settings UI must clear `api_base` and `kwargs` because both may be provider-specific.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ PRESETS_FILE = "presets.yaml"
|
|||
FALLBACK_PRESETS_FILE = "mode_presets_fallback.yaml"
|
||||
PROVIDER_METADATA_FILE = "provider_metadata.yaml"
|
||||
DEFAULT_PRESET_NAME = "Default"
|
||||
DEFAULT_VISION_TIMEOUT_SECONDS = 300
|
||||
DEFAULT_VISION_MAX_TOKENS = 2000
|
||||
MODEL_PRESET_CONFIG_KEY = "model_preset"
|
||||
PRESET_SCOPE_GLOBAL = "global"
|
||||
PRESET_SCOPE_PROJECT = "project"
|
||||
|
|
@ -25,6 +27,8 @@ IMPLICIT_PRESET_SLOT_DEFAULTS = {
|
|||
"vision": {
|
||||
"vision": True,
|
||||
"max_embeds": 10,
|
||||
"timeout": DEFAULT_VISION_TIMEOUT_SECONDS,
|
||||
"max_tokens": DEFAULT_VISION_MAX_TOKENS,
|
||||
"override_main": False,
|
||||
"rl_requests": 0,
|
||||
"rl_input": 0,
|
||||
|
|
@ -873,8 +877,18 @@ def build_vision_model(agent=None):
|
|||
cfg = get_vision_model_config(agent)
|
||||
mc = build_model_config(cfg, models.ModelType.CHAT)
|
||||
mc.vision = True
|
||||
kwargs = mc.build_kwargs()
|
||||
for key, default in (
|
||||
("timeout", DEFAULT_VISION_TIMEOUT_SECONDS),
|
||||
("max_tokens", DEFAULT_VISION_MAX_TOKENS),
|
||||
):
|
||||
value = cfg.get(key)
|
||||
if value not in (None, ""):
|
||||
kwargs[key] = _normalize_kwargs({key: value})[key]
|
||||
else:
|
||||
kwargs.setdefault(key, default)
|
||||
return models.get_chat_model(
|
||||
mc.provider, mc.name, model_config=mc, **mc.build_kwargs()
|
||||
mc.provider, mc.name, model_config=mc, **kwargs
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ const IMPLICIT_PRESET_SLOT_DEFAULTS = {
|
|||
vision: {
|
||||
vision: true,
|
||||
max_embeds: 10,
|
||||
timeout: 300,
|
||||
max_tokens: 2000,
|
||||
override_main: false,
|
||||
rl_requests: 0,
|
||||
rl_input: 0,
|
||||
|
|
@ -222,7 +224,23 @@ export const store = createStore("modelConfig", {
|
|||
const source = (rawPresets || []).filter(p => p && typeof p === 'object');
|
||||
const rawDefault = source.find(p => String(p.name || '').toLowerCase() === 'default') || {};
|
||||
const slot = value => ({ provider: '', name: '', api_key: '', api_base: '', kwargs: {}, ...(value || {}) });
|
||||
const visionSlot = value => ({ ...slot(value), vision: true, max_embeds: Number(value?.max_embeds ?? 10), override_main: !!value?.override_main });
|
||||
const visionSlot = value => {
|
||||
const normalized = slot(value);
|
||||
const kwargs = { ...(normalized.kwargs || {}) };
|
||||
const timeout = Number(value?.timeout ?? kwargs.timeout ?? 300);
|
||||
const maxTokens = Number(value?.max_tokens ?? kwargs.max_tokens ?? 2000);
|
||||
delete kwargs.timeout;
|
||||
delete kwargs.max_tokens;
|
||||
return {
|
||||
...normalized,
|
||||
vision: true,
|
||||
max_embeds: Number(value?.max_embeds ?? 10),
|
||||
timeout,
|
||||
max_tokens: maxTokens,
|
||||
override_main: !!value?.override_main,
|
||||
kwargs,
|
||||
};
|
||||
};
|
||||
const defaultConfig = {
|
||||
chat_model: slot(rawDefault.chat),
|
||||
vision_model: hasModelIdentity(rawDefault.vision) ? visionSlot(rawDefault.vision) : {},
|
||||
|
|
@ -234,10 +252,11 @@ export const store = createStore("modelConfig", {
|
|||
const effective = String(p.name || '').toLowerCase() === 'default'
|
||||
? defaultConfig
|
||||
: configFromPreset(p, defaultConfig, true);
|
||||
const vision = visionSlot(effective.vision_model);
|
||||
return {
|
||||
name: p.name || '',
|
||||
chat: { ...slot(effective.chat_model), _kwargs_text: kwargsToText(effective.chat_model?.kwargs) },
|
||||
vision: { ...visionSlot(effective.vision_model), _kwargs_text: kwargsToText(effective.vision_model?.kwargs) },
|
||||
vision: { ...vision, _kwargs_text: kwargsToText(vision.kwargs) },
|
||||
utility: { ...slot(effective.utility_model), _kwargs_text: kwargsToText(effective.utility_model?.kwargs) },
|
||||
embedding: { ...slot(effective.embedding_model), _kwargs_text: kwargsToText(effective.embedding_model?.kwargs) },
|
||||
};
|
||||
|
|
@ -360,7 +379,7 @@ export const store = createStore("modelConfig", {
|
|||
|| this.presets[0]
|
||||
|| {
|
||||
chat: { provider: '', name: '', api_base: '', kwargs: {}, _kwargs_text: '' },
|
||||
vision: { provider: '', name: '', api_base: '', vision: true, max_embeds: 10, override_main: false, kwargs: {}, _kwargs_text: '' },
|
||||
vision: { provider: '', name: '', api_base: '', vision: true, max_embeds: 10, timeout: 300, max_tokens: 2000, override_main: false, kwargs: {}, _kwargs_text: '' },
|
||||
utility: { provider: '', name: '', api_base: '', kwargs: {}, _kwargs_text: '' },
|
||||
embedding: { provider: '', name: '', api_base: '', kwargs: {}, _kwargs_text: '' },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<!--
|
||||
Reusable model configuration field set.
|
||||
Parent x-data scope must provide:
|
||||
model — reactive object with provider, name, api_key, api_base, ctx_length, ctx_history, ctx_input, vision, max_embeds, rl_requests, rl_input, rl_output, kwargs, _kwargs_text
|
||||
model — reactive object with provider, name, api_key, api_base, ctx_length, ctx_history, ctx_input, vision, max_embeds, timeout, max_tokens, rl_requests, rl_input, rl_output, kwargs, _kwargs_text
|
||||
modelType — 'chat' | 'vision' | 'utility' | 'embedding'
|
||||
providers — array of { value, label }
|
||||
searchType — 'chat' | 'embedding'
|
||||
|
|
@ -224,14 +224,38 @@
|
|||
</template>
|
||||
|
||||
<template x-if="modelType === 'vision'">
|
||||
<div class="field">
|
||||
<div class="field-label">
|
||||
<div class="field-title">Max embeds</div>
|
||||
<div class="field-description">Maximum number of images sent to the Vision Model in one call. Set to 0 for unlimited.</div>
|
||||
<div>
|
||||
<div class="field">
|
||||
<div class="field-label">
|
||||
<div class="field-title">Max embeds</div>
|
||||
<div class="field-description">Maximum number of images sent to the Vision Model in one call. Set to 0 for unlimited.</div>
|
||||
</div>
|
||||
<div class="field-control">
|
||||
<input type="number" min="0" x-model.number="model.max_embeds" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-control">
|
||||
<input type="number" min="0" x-model.number="model.max_embeds" />
|
||||
|
||||
<div class="field">
|
||||
<div class="field-label">
|
||||
<div class="field-title">Timeout (seconds)</div>
|
||||
<div class="field-description">How long to wait for the Vision Model.</div>
|
||||
</div>
|
||||
<div class="field-control">
|
||||
<input type="number" min="1" x-model.number="model.timeout" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="field-label">
|
||||
<div class="field-title">Max tokens</div>
|
||||
<div class="field-description">Maximum output tokens for each delegated vision call.</div>
|
||||
</div>
|
||||
<div class="field-control">
|
||||
<input type="number" min="1" x-model.number="model.max_tokens" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="field-description">To customize the vision prompt, use Agent Editor > Advanced > Prompt files and edit <code>fw.vision_load.md</code>.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1130,6 +1130,104 @@ def test_preset_vision_slot_is_optional_and_never_inherited(monkeypatch, tmp_pat
|
|||
assert "default-vision" not in str(with_sidecar["vision_model"])
|
||||
|
||||
|
||||
def test_vision_call_limits_are_preset_owned_and_default_clean(monkeypatch, tmp_path):
|
||||
_prepare_a0_tree(monkeypatch, tmp_path)
|
||||
|
||||
from plugins._model_config.helpers import model_config
|
||||
|
||||
cleaned = model_config.clean_presets_for_file(
|
||||
[
|
||||
{
|
||||
"name": "Default limits",
|
||||
"vision": {
|
||||
"provider": "openrouter",
|
||||
"name": "vision-default",
|
||||
"timeout": 300,
|
||||
"max_tokens": 2000,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "Tuned limits",
|
||||
"vision": {
|
||||
"provider": "openrouter",
|
||||
"name": "vision-tuned",
|
||||
"timeout": 45,
|
||||
"max_tokens": 512,
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
assert cleaned[0]["vision"] == {
|
||||
"provider": "openrouter",
|
||||
"name": "vision-default",
|
||||
}
|
||||
assert cleaned[1]["vision"]["timeout"] == 45
|
||||
assert cleaned[1]["vision"]["max_tokens"] == 512
|
||||
|
||||
|
||||
def test_vision_model_build_applies_only_its_preset_call_limits(monkeypatch):
|
||||
from plugins._model_config.helpers import model_config
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_get_chat_model(provider, name, **kwargs):
|
||||
calls.append((provider, name, kwargs))
|
||||
return kwargs
|
||||
|
||||
monkeypatch.setattr(model_config.models, "get_chat_model", fake_get_chat_model)
|
||||
|
||||
cases = [
|
||||
(
|
||||
{"provider": "openrouter", "name": "vision-default"},
|
||||
{"timeout": 300, "max_tokens": 2000},
|
||||
),
|
||||
(
|
||||
{
|
||||
"provider": "openrouter",
|
||||
"name": "vision-legacy-kwargs",
|
||||
"kwargs": {"timeout": 90, "max_tokens": 1024},
|
||||
},
|
||||
{"timeout": 90, "max_tokens": 1024},
|
||||
),
|
||||
(
|
||||
{
|
||||
"provider": "openrouter",
|
||||
"name": "vision-tuned",
|
||||
"timeout": "45",
|
||||
"max_tokens": "512",
|
||||
"kwargs": {
|
||||
"timeout": 90,
|
||||
"max_tokens": 1024,
|
||||
"temperature": 0.1,
|
||||
},
|
||||
},
|
||||
{"timeout": 45, "max_tokens": 512},
|
||||
),
|
||||
]
|
||||
|
||||
for config, expected in cases:
|
||||
monkeypatch.setattr(
|
||||
model_config,
|
||||
"get_vision_model_config",
|
||||
lambda _agent=None, config=config: config,
|
||||
)
|
||||
built = model_config.build_vision_model()
|
||||
assert built["timeout"] == expected["timeout"]
|
||||
assert built["max_tokens"] == expected["max_tokens"]
|
||||
|
||||
assert calls[-1][2]["temperature"] == 0.1
|
||||
|
||||
monkeypatch.setattr(
|
||||
model_config,
|
||||
"get_chat_model_config",
|
||||
lambda _agent=None: {"provider": "openrouter", "name": "main"},
|
||||
)
|
||||
main = model_config.build_chat_model()
|
||||
assert "timeout" not in main
|
||||
assert "max_tokens" not in main
|
||||
|
||||
|
||||
def test_legacy_utility_preset_defaults_preserve_tuning_but_clear_kwargs(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ def test_preset_editor_nests_one_conditional_vision_selector_in_main() -> None:
|
|||
override_start = model_field.index('<div class="field-title">Use separate Vision Model</div>')
|
||||
context_start = model_field.index('<div class="field-title">Context window size</div>')
|
||||
advanced_start = model_field.index('<!-- Advanced Settings (collapsed by default) -->')
|
||||
vision_advanced_start = model_field.index('<template x-if="modelType === \'vision\'">')
|
||||
vision_advanced_end = model_field.index('<!-- Utility-specific: ctx_input slider -->')
|
||||
vision_advanced = model_field[vision_advanced_start:vision_advanced_end]
|
||||
|
||||
assert '<div class="section-title">Vision Model</div>' not in preset_modal
|
||||
assert preset_modal.count('class="vision-sidecar-selector"') == 1
|
||||
|
|
@ -115,6 +118,24 @@ def test_preset_editor_nests_one_conditional_vision_selector_in_main() -> None:
|
|||
assert "When enabled, vision_load uses the preset's Vision Model" not in model_field
|
||||
assert 'x-model="visionModel.override_main"' in model_field
|
||||
assert '<div class="advanced-section" x-data="{ advOpen: false }">' in model_field
|
||||
assert advanced_start < vision_advanced_start
|
||||
assert model_field.count('<div class="field-title">Timeout (seconds)</div>') == 1
|
||||
assert model_field.count('<div class="field-title">Max tokens</div>') == 1
|
||||
assert 'x-model.number="model.timeout"' in vision_advanced
|
||||
assert 'x-model.number="model.max_tokens"' in vision_advanced
|
||||
assert "How long to wait for the Vision Model." in vision_advanced
|
||||
assert "Maximum output tokens for each delegated vision call." in vision_advanced
|
||||
assert "Agent Editor > Advanced > Prompt files" in vision_advanced
|
||||
assert "fw.vision_load.md" in vision_advanced
|
||||
assert "delegated_system" not in model_field
|
||||
assert "vision prompt" not in model_field[:vision_advanced_start].lower()
|
||||
assert "timeout: 300" in preset_store
|
||||
assert "max_tokens: 2000" in preset_store
|
||||
assert "value?.timeout ?? kwargs.timeout ?? 300" in preset_store
|
||||
assert "value?.max_tokens ?? kwargs.max_tokens ?? 2000" in preset_store
|
||||
assert "delete kwargs.timeout;" in preset_store
|
||||
assert "delete kwargs.max_tokens;" in preset_store
|
||||
assert "_kwargs_text: kwargsToText(vision.kwargs)" in preset_store
|
||||
assert "model-preset-row-nested" in preset_overview
|
||||
assert "model.title === 'Vision'" in preset_overview
|
||||
assert preset_overview.count("model.title !== 'Vision'") == 2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue