hermes: keep foreground turns on hosted models
All checks were successful
Tests / Declarative: Post Actions passed: 255

This commit is contained in:
jenkins 2026-08-12 09:57:46 -03:00
parent cd06751b66
commit 8471ffb3f8
3 changed files with 73 additions and 56 deletions

View File

@ -191,7 +191,7 @@ data:
classifier_target = "classifier" classifier_target = "classifier"
# Switchyard falls through this list after a request-local target failure. # Switchyard falls through this list after a request-local target failure.
# Keep both xhigh providers first so recovery can escalate, never downgrade. # Keep both xhigh providers first so recovery can escalate, never downgrade.
targets = ["codex_sol_xhigh", "claude_opus_xhigh", "codex_sol_high", "claude_sonnet_high", "claude_opus_high", "codex_terra_high", "codex_sol_medium", "claude_sonnet_medium", "codex_terra_medium", "local_qwen_medium", "codex_terra_low", "codex_luna_low", "claude_haiku_low", "local_qwen_low"] targets = ["codex_sol_xhigh", "claude_opus_xhigh", "codex_sol_high", "claude_sonnet_high", "claude_opus_high", "codex_terra_high", "codex_sol_medium", "claude_sonnet_medium", "codex_terra_medium", "codex_terra_low", "codex_luna_low", "claude_haiku_low"]
default_target = "codex_terra_medium" default_target = "codex_terra_medium"
session_affinity = false session_affinity = false
recent_turn_window = 4 recent_turn_window = 4
@ -225,29 +225,30 @@ data:
3. Route image creation and editing before general provider preference. 3. Route image creation and editing before general provider preference.
When image-generation/edit tools are available and the user asks to create, When image-generation/edit tools are available and the user asks to create,
transform, restore, colorize, or continue editing an image: select transform, restore, colorize, or continue editing an image: select
local_qwen_medium when local generation is requested; select codex_terra_medium. The image tool—not the conversational model—honors the
codex_terra_medium when OpenAI or hosted generation is requested; otherwise user's local, OpenAI/hosted, or AUTO image-backend choice. Do not select a
choose either of those two medium targets. Do not select a Claude target for local Qwen or Claude target for an image-tool boundary: local Qwen cannot
an image-tool boundary: Anthropic supplies the conversation model, not an reliably carry the full Hermes tool context, and Anthropic supplies the
image backend, and Claude capacity must not block image execution. conversation model rather than either configured image backend.
4. Use local Qwen only at low or medium for low-risk conversation, 4. Local Qwen is the mandatory routing classifier, but its 8K context and
formatting, lookup, and continuity. Prefer Codex for implementation, tool protocol are not eligible for foreground Hermes execution. Prefer
debugging, tests, commands, and repository work. Prefer Claude for Codex for implementation, debugging, tests, commands, repository work, and
architecture, ambiguity, synthesis, risk analysis, adversarial analysis, image-tool invocation. Prefer Claude for architecture, ambiguity,
and independent review. Anthropic means Claude; OpenAI means Codex. Never synthesis, risk analysis, adversarial analysis, and independent review.
select a provider stated to be unavailable, failed, exhausted, rate-limited, Anthropic means Claude; OpenAI means Codex. Never select a provider stated
or out of capacity; use the other provider at the same floor. to be unavailable, failed, exhausted, rate-limited, or out of capacity; use
the other hosted provider at the same floor.
5. Map exactly: Codex low=codex_luna_low, medium=codex_terra_medium, 5. Map exactly: Codex low=codex_luna_low, medium=codex_terra_medium,
high=codex_sol_high, xhigh=codex_sol_xhigh; Claude low=claude_haiku_low, high=codex_sol_high, xhigh=codex_sol_xhigh; Claude low=claude_haiku_low,
medium=claude_sonnet_medium, high=claude_sonnet_high, medium=claude_sonnet_medium, high=claude_sonnet_high,
xhigh=claude_opus_xhigh; local low=local_qwen_low and local xhigh=claude_opus_xhigh. Re-evaluate every boundary and resolve "continue"
medium=local_qwen_medium. Re-evaluate every boundary and resolve "continue" or "do it" from recent context. The manual local route remains available
or "do it" from recent context. only when the user explicitly selects it.
""" """
response_schema = ''' response_schema = '''
{"type":"object","properties":{"decision":{"type":"object","properties":{"target":{"type":"string","enum":["codex_terra_medium","claude_sonnet_medium","codex_luna_low","claude_haiku_low","local_qwen_low","local_qwen_medium","codex_terra_low","codex_terra_high","codex_sol_medium","codex_sol_high","codex_sol_xhigh","claude_sonnet_high","claude_opus_high","claude_opus_xhigh"]}},"required":["target"],"additionalProperties":false}},"required":["decision"],"additionalProperties":false} {"type":"object","properties":{"decision":{"type":"object","properties":{"target":{"type":"string","enum":["codex_terra_medium","claude_sonnet_medium","codex_luna_low","claude_haiku_low","codex_terra_low","codex_terra_high","codex_sol_medium","codex_sol_high","codex_sol_xhigh","claude_sonnet_high","claude_opus_high","claude_opus_xhigh"]}},"required":["target"],"additionalProperties":false}},"required":["decision"],"additionalProperties":false}
''' '''
[routes.auto_fast.policy] [routes.auto_fast.policy]
@ -261,7 +262,7 @@ data:
classifier_target = "classifier" classifier_target = "classifier"
# Switchyard falls through this list after a request-local target failure. # Switchyard falls through this list after a request-local target failure.
# Keep both xhigh providers first so recovery can escalate, never downgrade. # Keep both xhigh providers first so recovery can escalate, never downgrade.
targets = ["codex_sol_xhigh", "claude_opus_xhigh", "codex_sol_high", "claude_sonnet_high", "claude_opus_high", "codex_terra_high", "codex_sol_medium", "claude_sonnet_medium", "codex_terra_medium", "local_qwen_medium", "codex_terra_low", "codex_luna_low", "claude_haiku_low", "local_qwen_low"] targets = ["codex_sol_xhigh", "claude_opus_xhigh", "codex_sol_high", "claude_sonnet_high", "claude_opus_high", "codex_terra_high", "codex_sol_medium", "claude_sonnet_medium", "codex_terra_medium", "codex_terra_low", "codex_luna_low", "claude_haiku_low"]
default_target = "codex_terra_medium" default_target = "codex_terra_medium"
session_affinity = false session_affinity = false
recent_turn_window = 4 recent_turn_window = 4
@ -296,29 +297,30 @@ data:
3. Route image creation and editing before general provider preference. 3. Route image creation and editing before general provider preference.
When image-generation/edit tools are available and the user asks to create, When image-generation/edit tools are available and the user asks to create,
transform, restore, colorize, or continue editing an image: select transform, restore, colorize, or continue editing an image: select
local_qwen_medium when local generation is requested; select codex_terra_medium. The image tool—not the conversational model—honors the
codex_terra_medium when OpenAI or hosted generation is requested; otherwise user's local, OpenAI/hosted, or AUTO image-backend choice. Do not select a
choose either of those two medium targets. Do not select a Claude target for local Qwen or Claude target for an image-tool boundary: local Qwen cannot
an image-tool boundary: Anthropic supplies the conversation model, not an reliably carry the full Hermes tool context, and Anthropic supplies the
image backend, and Claude capacity must not block image execution. conversation model rather than either configured image backend.
4. Use local Qwen only at low or medium for low-risk conversation, 4. Local Qwen is the mandatory routing classifier, but its 8K context and
formatting, lookup, and continuity. Prefer Codex for implementation, tool protocol are not eligible for foreground Hermes execution. Prefer
debugging, tests, commands, and repository work. Prefer Claude for Codex for implementation, debugging, tests, commands, repository work, and
architecture, ambiguity, synthesis, risk analysis, adversarial analysis, image-tool invocation. Prefer Claude for architecture, ambiguity,
and independent review. Anthropic means Claude; OpenAI means Codex. Never synthesis, risk analysis, adversarial analysis, and independent review.
select a provider stated to be unavailable, failed, exhausted, rate-limited, Anthropic means Claude; OpenAI means Codex. Never select a provider stated
or out of capacity; use the other provider at the same floor. to be unavailable, failed, exhausted, rate-limited, or out of capacity; use
the other hosted provider at the same floor.
5. Map exactly: Codex low=codex_luna_low, medium=codex_terra_medium, 5. Map exactly: Codex low=codex_luna_low, medium=codex_terra_medium,
high=codex_sol_high, xhigh=codex_sol_xhigh; Claude low=claude_haiku_low, high=codex_sol_high, xhigh=codex_sol_xhigh; Claude low=claude_haiku_low,
medium=claude_sonnet_medium, high=claude_sonnet_high, medium=claude_sonnet_medium, high=claude_sonnet_high,
xhigh=claude_opus_xhigh; local low=local_qwen_low and local xhigh=claude_opus_xhigh. Re-evaluate every boundary and resolve "continue"
medium=local_qwen_medium. Re-evaluate every boundary and resolve "continue" or "do it" from recent context. The manual local route remains available
or "do it" from recent context. only when the user explicitly selects it.
""" """
response_schema = ''' response_schema = '''
{"type":"object","properties":{"decision":{"type":"object","properties":{"target":{"type":"string","enum":["codex_terra_medium","claude_sonnet_medium","codex_luna_low","claude_haiku_low","local_qwen_low","local_qwen_medium","codex_terra_low","codex_terra_high","codex_sol_medium","codex_sol_high","codex_sol_xhigh","claude_sonnet_high","claude_opus_high","claude_opus_xhigh"]}},"required":["target"],"additionalProperties":false}},"required":["decision"],"additionalProperties":false} {"type":"object","properties":{"decision":{"type":"object","properties":{"target":{"type":"string","enum":["codex_terra_medium","claude_sonnet_medium","codex_luna_low","claude_haiku_low","codex_terra_low","codex_terra_high","codex_sol_medium","codex_sol_high","codex_sol_xhigh","claude_sonnet_high","claude_opus_high","claude_opus_xhigh"]}},"required":["target"],"additionalProperties":false}},"required":["decision"],"additionalProperties":false}
''' '''
[routes.auto_balanced.policy] [routes.auto_balanced.policy]
@ -365,7 +367,7 @@ data:
available provider or model wins at the safety floor. available provider or model wins at the safety floor.
3. This quality-first triage route must use a hosted Codex or Claude target. 3. This quality-first triage route must use a hosted Codex or Claude target.
The local Qwen model remains the routing classifier, but its 32K context and The local Qwen model remains the routing classifier, but its 8K context and
tool protocol are not eligible for foreground triage execution. Prefer tool protocol are not eligible for foreground triage execution. Prefer
Codex for implementation, debugging, tests, commands, and repository work. Codex for implementation, debugging, tests, commands, and repository work.
Prefer Claude for diagnosis, architecture, ambiguity, synthesis, risk Prefer Claude for diagnosis, architecture, ambiguity, synthesis, risk
@ -429,7 +431,7 @@ data:
available provider or model wins at the safety floor. available provider or model wins at the safety floor.
3. This owner-only engineering route must use a hosted Codex or Claude 3. This owner-only engineering route must use a hosted Codex or Claude
target. The local Qwen model remains the routing classifier, but its 32K target. The local Qwen model remains the routing classifier, but its 8K
context and tool protocol are not eligible for foreground agent execution. context and tool protocol are not eligible for foreground agent execution.
Prefer Codex for implementation, debugging, tests, commands, and repository Prefer Codex for implementation, debugging, tests, commands, and repository
changes. Prefer Claude for architecture, ambiguity, synthesis, risk changes. Prefer Claude for architecture, ambiguity, synthesis, risk
@ -574,8 +576,8 @@ data:
[routes.manual_codex_luna] [routes.manual_codex_luna]
id = "atlas/manual/codex/luna" id = "atlas/manual/codex/luna"
type = "random" type = "random"
targets = ["codex_luna_low", "claude_haiku_low", "codex_terra_medium", "claude_sonnet_medium", "local_qwen_low"] targets = ["codex_luna_low", "claude_haiku_low", "codex_terra_medium", "claude_sonnet_medium"]
weights = [1, 0, 0, 0, 0] weights = [1, 0, 0, 0]
context_window = 272000 context_window = 272000
tool_calling = true tool_calling = true
reasoning = true reasoning = true
@ -583,8 +585,8 @@ data:
[routes.manual_codex_terra] [routes.manual_codex_terra]
id = "atlas/manual/codex/terra" id = "atlas/manual/codex/terra"
type = "random" type = "random"
targets = ["codex_terra_medium", "claude_sonnet_medium", "codex_sol_high", "claude_sonnet_high", "local_qwen_medium"] targets = ["codex_terra_medium", "claude_sonnet_medium", "codex_sol_high", "claude_sonnet_high"]
weights = [1, 0, 0, 0, 0] weights = [1, 0, 0, 0]
context_window = 272000 context_window = 272000
tool_calling = true tool_calling = true
reasoning = true reasoning = true
@ -592,8 +594,8 @@ data:
[routes.manual_codex_sol] [routes.manual_codex_sol]
id = "atlas/manual/codex/sol" id = "atlas/manual/codex/sol"
type = "random" type = "random"
targets = ["codex_sol_high", "claude_opus_high", "claude_sonnet_high", "codex_terra_high", "local_qwen_medium"] targets = ["codex_sol_high", "claude_opus_high", "claude_sonnet_high", "codex_terra_high"]
weights = [1, 0, 0, 0, 0] weights = [1, 0, 0, 0]
context_window = 272000 context_window = 272000
tool_calling = true tool_calling = true
reasoning = true reasoning = true
@ -601,8 +603,8 @@ data:
[routes.manual_claude_haiku] [routes.manual_claude_haiku]
id = "atlas/manual/claude/haiku" id = "atlas/manual/claude/haiku"
type = "random" type = "random"
targets = ["claude_haiku_low", "codex_luna_low", "claude_sonnet_medium", "codex_terra_medium", "local_qwen_low"] targets = ["claude_haiku_low", "codex_luna_low", "claude_sonnet_medium", "codex_terra_medium"]
weights = [1, 0, 0, 0, 0] weights = [1, 0, 0, 0]
context_window = 272000 context_window = 272000
tool_calling = true tool_calling = true
reasoning = true reasoning = true
@ -610,8 +612,8 @@ data:
[routes.manual_claude_sonnet] [routes.manual_claude_sonnet]
id = "atlas/manual/claude/sonnet" id = "atlas/manual/claude/sonnet"
type = "random" type = "random"
targets = ["claude_sonnet_high", "codex_sol_high", "claude_opus_high", "codex_terra_high", "local_qwen_medium"] targets = ["claude_sonnet_high", "codex_sol_high", "claude_opus_high", "codex_terra_high"]
weights = [1, 0, 0, 0, 0] weights = [1, 0, 0, 0]
context_window = 272000 context_window = 272000
tool_calling = true tool_calling = true
reasoning = true reasoning = true
@ -619,8 +621,8 @@ data:
[routes.manual_claude_opus] [routes.manual_claude_opus]
id = "atlas/manual/claude/opus" id = "atlas/manual/claude/opus"
type = "random" type = "random"
targets = ["claude_opus_high", "codex_sol_high", "claude_sonnet_high", "codex_terra_high", "local_qwen_medium"] targets = ["claude_opus_high", "codex_sol_high", "claude_sonnet_high", "codex_terra_high"]
weights = [1, 0, 0, 0, 0] weights = [1, 0, 0, 0]
context_window = 272000 context_window = 272000
tool_calling = true tool_calling = true
reasoning = true reasoning = true

View File

@ -22,7 +22,7 @@ spec:
labels: labels:
app: hermes-switchyard app: hermes-switchyard
annotations: annotations:
ai.bstein.dev/config-rev: "20260812-classifier-context" ai.bstein.dev/config-rev: "20260812-hosted-foreground"
prometheus.io/scrape: "true" prometheus.io/scrape: "true"
prometheus.io/port: "9005" prometheus.io/port: "9005"
prometheus.io/path: /metrics prometheus.io/path: /metrics

View File

@ -1135,22 +1135,37 @@ def test_local_flux_runtime_and_gpu_handoff_are_flux_managed():
leading_targets = set(routes[route_name]["targets"][:2]) leading_targets = set(routes[route_name]["targets"][:2])
assert leading_targets == {"codex_sol_xhigh", "claude_opus_xhigh"} assert leading_targets == {"codex_sol_xhigh", "claude_opus_xhigh"}
assert "max_output_tokens" not in routes[route_name] assert "max_output_tokens" not in routes[route_name]
for route_name in ("auto_deep", "auto_maximum"): for route_name in ("auto_fast", "auto_balanced", "auto_deep", "auto_maximum"):
targets = routes[route_name]["targets"] targets = routes[route_name]["targets"]
selector_targets = routes[route_name]["response_schema"] selector_targets = routes[route_name]["response_schema"]
assert not any(target.startswith("local_") for target in targets) assert not any(target.startswith("local_") for target in targets)
assert not any(target.endswith("_low") for target in targets)
assert "local_qwen" not in selector_targets assert "local_qwen" not in selector_targets
assert "_low" not in selector_targets
assert "not eligible for foreground" in routes[route_name]["prompt"] assert "not eligible for foreground" in routes[route_name]["prompt"]
for route_name in ("auto_fast", "auto_balanced", "manual_local_qwen"): for route_name in ("auto_deep", "auto_maximum"):
assert any( targets = routes[route_name]["targets"]
selector_targets = routes[route_name]["response_schema"]
assert not any(target.endswith("_low") for target in targets)
assert "_low" not in selector_targets
assert any(
target.startswith("local_")
for target in routes["manual_local_qwen"]["targets"]
)
for route_name in (
"manual_codex_luna",
"manual_codex_terra",
"manual_codex_sol",
"manual_claude_haiku",
"manual_claude_sonnet",
"manual_claude_opus",
):
assert not any(
target.startswith("local_") for target in routes[route_name]["targets"] target.startswith("local_") for target in routes[route_name]["targets"]
) )
for route_name in ("auto_fast", "auto_balanced"): for route_name in ("auto_fast", "auto_balanced"):
prompt = routes[route_name]["prompt"] prompt = routes[route_name]["prompt"]
assert "Route image creation and editing before general provider" in prompt assert "image tool—not the conversational model" in prompt
assert "Do not select a Claude target for" in prompt assert "Do not select a" in prompt
assert "local Qwen or Claude target" in prompt
assert "max_output_tokens" not in routes["worker_auto_maximum"] assert "max_output_tokens" not in routes["worker_auto_maximum"]
model_gate = _documents(HERMES / "model-gate-configmap.yaml")[0]["data"][ model_gate = _documents(HERMES / "model-gate-configmap.yaml")[0]["data"][
"model_gate.py" "model_gate.py"