Skip to content

Commit 75002a1

Browse files
Merge pull request #55 from Annotation-Garden/develop
Release v0.6.2-alpha: BYOK model selection and provider auto-clear
2 parents cf815ff + 3e22f0c commit 75002a1

File tree

5 files changed

+331
-24
lines changed

5 files changed

+331
-24
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Manual Test Plan: BYOK Model Selection
2+
3+
This document describes how to manually test the BYOK model/provider/temperature selection feature after PR #54 is merged.
4+
5+
## Prerequisites
6+
7+
1. Have a valid OpenRouter API key (get one at https://openrouter.ai)
8+
2. Have the HEDit CLI installed: `pip install hedit` or `pip install -e .`
9+
3. Know the API endpoint (e.g., `https://api.annotation.garden/hedit` or local `http://localhost:38427`)
10+
11+
## Test 1: Request Body Model Selection (API)
12+
13+
Test that model settings in the request body are used.
14+
15+
```bash
16+
# Set your API key
17+
export OPENROUTER_KEY="sk-or-v1-your-key-here"
18+
19+
# Test with custom model in request body
20+
curl -X POST https://api.annotation.garden/hedit/annotate \
21+
-H "Content-Type: application/json" \
22+
-H "X-OpenRouter-Key: $OPENROUTER_KEY" \
23+
-d '{
24+
"description": "A red circle appears on the left side of the screen",
25+
"model": "openai/gpt-4o-mini",
26+
"temperature": 0.3
27+
}'
28+
```
29+
30+
**Expected**: Should use `gpt-4o-mini` model (verify in OpenRouter dashboard usage logs).
31+
32+
## Test 2: Header-Based Model Selection (API)
33+
34+
Test that model settings in headers are used as fallback.
35+
36+
```bash
37+
# Test with custom model in headers
38+
curl -X POST https://api.annotation.garden/hedit/annotate \
39+
-H "Content-Type: application/json" \
40+
-H "X-OpenRouter-Key: $OPENROUTER_KEY" \
41+
-H "X-OpenRouter-Model: anthropic/claude-3-haiku-20240307" \
42+
-H "X-OpenRouter-Temperature: 0.1" \
43+
-d '{
44+
"description": "A blue square fades in at the center"
45+
}'
46+
```
47+
48+
**Expected**: Should use `claude-3-haiku` model.
49+
50+
## Test 3: Request Body Overrides Headers
51+
52+
Test that request body has higher priority than headers.
53+
54+
```bash
55+
curl -X POST https://api.annotation.garden/hedit/annotate \
56+
-H "Content-Type: application/json" \
57+
-H "X-OpenRouter-Key: $OPENROUTER_KEY" \
58+
-H "X-OpenRouter-Model: anthropic/claude-3-haiku-20240307" \
59+
-d '{
60+
"description": "A green triangle rotates",
61+
"model": "openai/gpt-4o-mini"
62+
}'
63+
```
64+
65+
**Expected**: Should use `gpt-4o-mini` (body), NOT `claude-3-haiku` (header).
66+
67+
## Test 4: CLI Model Selection
68+
69+
Test the CLI with `--model` flag.
70+
71+
```bash
72+
# Initialize with your key
73+
hedit init --api-key $OPENROUTER_KEY
74+
75+
# Test with custom model
76+
hedit annotate "A loud beep sound plays" --model openai/gpt-4o-mini --temperature 0.2
77+
```
78+
79+
**Expected**: Should use specified model.
80+
81+
## Test 5: Image Annotation with Vision Model
82+
83+
Test image annotation with custom vision model.
84+
85+
```bash
86+
# Create a test image or use any image file
87+
curl -X POST https://api.annotation.garden/hedit/annotate-from-image \
88+
-H "Content-Type: application/json" \
89+
-H "X-OpenRouter-Key: $OPENROUTER_KEY" \
90+
-d "{
91+
\"image\": \"data:image/png;base64,$(base64 -i test_image.png)\",
92+
\"model\": \"openai/gpt-4o\",
93+
\"vision_model\": \"openai/gpt-4o\",
94+
\"temperature\": 0.3
95+
}"
96+
```
97+
98+
**Expected**: Should use specified vision model for description.
99+
100+
## Test 6: Server Default Fallback
101+
102+
Test that without BYOK, server uses its defaults (this should already work).
103+
104+
```bash
105+
# Using server API key (if you have one)
106+
curl -X POST https://api.annotation.garden/hedit/annotate \
107+
-H "Content-Type: application/json" \
108+
-H "X-API-Key: your-server-api-key" \
109+
-d '{
110+
"description": "A warning message appears"
111+
}'
112+
```
113+
114+
**Expected**: Should use server's default model from environment variables.
115+
116+
## Test 7: Temperature Range Validation
117+
118+
Test that temperature validation works.
119+
120+
```bash
121+
# Invalid temperature (should fail validation)
122+
curl -X POST https://api.annotation.garden/hedit/annotate \
123+
-H "Content-Type: application/json" \
124+
-H "X-OpenRouter-Key: $OPENROUTER_KEY" \
125+
-d '{
126+
"description": "Test",
127+
"temperature": 1.5
128+
}'
129+
```
130+
131+
**Expected**: Should return 422 validation error (temperature must be 0.0-1.0).
132+
133+
## Test 8: Provider Selection
134+
135+
Test provider preference (e.g., Cerebras for fast inference).
136+
137+
```bash
138+
curl -X POST https://api.annotation.garden/hedit/annotate \
139+
-H "Content-Type: application/json" \
140+
-H "X-OpenRouter-Key: $OPENROUTER_KEY" \
141+
-d '{
142+
"description": "A participant presses a button",
143+
"model": "openai/gpt-oss-120b",
144+
"provider": "Cerebras"
145+
}'
146+
```
147+
148+
**Expected**: Should route through Cerebras provider (faster inference).
149+
150+
## Verification
151+
152+
For all tests, verify:
153+
1. The request succeeds (HTTP 200)
154+
2. Valid HED annotation is returned
155+
3. Check OpenRouter dashboard to confirm which model was used
156+
4. Response time may vary by model/provider
157+
158+
## Notes
159+
160+
- The model parameter overrides ALL agents (annotation, evaluation, assessment)
161+
- Per-agent model selection is not yet supported via API (future enhancement)
162+
- Invalid model names will result in OpenRouter errors

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "hedit"
7-
version = "0.6.1-alpha2"
7+
version = "0.6.2-alpha"
88
description = "Multi-agent system for HED annotation generation and validation"
99
readme = "README.md"
1010
requires-python = ">=3.12"

src/api/main.py

Lines changed: 118 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,30 +48,60 @@
4848
_byok_config: dict = {}
4949

5050

51-
def create_byok_workflow(openrouter_key: str) -> HedAnnotationWorkflow:
51+
def create_byok_workflow(
52+
openrouter_key: str,
53+
model: str | None = None,
54+
provider: str | None = None,
55+
temperature: float | None = None,
56+
) -> HedAnnotationWorkflow:
5257
"""Create a workflow instance using the user's OpenRouter key (BYOK mode).
5358
5459
Args:
5560
openrouter_key: User's OpenRouter API key
61+
model: Override model for all agents (uses server default if None)
62+
provider: Override provider preference (uses server default if None)
63+
temperature: Override LLM temperature (uses server default if None)
5664
5765
Returns:
58-
Configured HedAnnotationWorkflow using the user's key
66+
Configured HedAnnotationWorkflow using the user's key and model settings
5967
"""
6068
global _byok_config
6169

6270
# Get configuration (cached from server startup)
63-
llm_temperature = _byok_config.get("temperature", 0.1)
64-
provider_preference = _byok_config.get("provider_preference")
6571
schema_dir = _byok_config.get("schema_dir")
6672
validator_path = _byok_config.get("validator_path")
6773
use_js_validator = _byok_config.get("use_js_validator", True)
6874

69-
# Get model configuration from headers or use defaults
70-
annotation_model = get_model_name(os.getenv("ANNOTATION_MODEL", "openai/gpt-oss-120b"))
71-
evaluation_model = get_model_name(os.getenv("EVALUATION_MODEL", "qwen/qwen3-235b-a22b-2507"))
72-
assessment_model = get_model_name(os.getenv("ASSESSMENT_MODEL", "openai/gpt-oss-120b"))
75+
# Use user-provided settings or fall back to server defaults
76+
llm_temperature = (
77+
temperature if temperature is not None else _byok_config.get("temperature", 0.1)
78+
)
79+
80+
# Provider logic:
81+
# - If user specifies a custom model, clear provider (Cerebras only works with default models)
82+
# - Unless user also explicitly specifies a provider
83+
if provider is not None:
84+
# User explicitly set provider (could be empty string to clear it)
85+
provider_preference = provider if provider else None
86+
elif model is not None:
87+
# User specified custom model but no provider → clear provider
88+
# (Cerebras only works with default models)
89+
provider_preference = None
90+
else:
91+
# No custom model or provider → use server defaults
92+
provider_preference = _byok_config.get("provider_preference")
93+
94+
# Get model configuration: user override > server env var > default
95+
default_annotation_model = os.getenv("ANNOTATION_MODEL", "openai/gpt-oss-120b")
96+
default_evaluation_model = os.getenv("EVALUATION_MODEL", "qwen/qwen3-235b-a22b-2507")
97+
default_assessment_model = os.getenv("ASSESSMENT_MODEL", "openai/gpt-oss-120b")
7398

74-
# Create LLMs with user's key
99+
# If user provides a model, use it for all agents (default override)
100+
annotation_model = get_model_name(model if model else default_annotation_model)
101+
evaluation_model = get_model_name(model if model else default_evaluation_model)
102+
assessment_model = get_model_name(model if model else default_assessment_model)
103+
104+
# Create LLMs with user's key and settings
75105
annotation_llm = create_openrouter_llm(
76106
model=annotation_model,
77107
api_key=openrouter_key,
@@ -102,23 +132,46 @@ def create_byok_workflow(openrouter_key: str) -> HedAnnotationWorkflow:
102132
)
103133

104134

105-
def create_byok_vision_agent(openrouter_key: str) -> VisionAgent:
135+
def create_byok_vision_agent(
136+
openrouter_key: str,
137+
vision_model: str | None = None,
138+
provider: str | None = None,
139+
temperature: float | None = None,
140+
) -> VisionAgent:
106141
"""Create a vision agent instance using the user's OpenRouter key (BYOK mode).
107142
108143
Args:
109144
openrouter_key: User's OpenRouter API key
145+
vision_model: Override vision model (uses server default if None)
146+
provider: Override provider preference (uses server default if None)
147+
temperature: Override temperature (uses 0.3 default if None)
110148
111149
Returns:
112-
Configured VisionAgent using the user's key
150+
Configured VisionAgent using the user's key and model settings
113151
"""
114-
vision_model = os.getenv("VISION_MODEL", "qwen/qwen3-vl-30b-a3b-instruct")
115-
vision_provider = os.getenv("VISION_PROVIDER", "deepinfra/fp8")
152+
# Use user-provided settings or fall back to server defaults
153+
default_vision_model = os.getenv("VISION_MODEL", "qwen/qwen3-vl-30b-a3b-instruct")
154+
default_vision_provider = os.getenv("VISION_PROVIDER", "deepinfra/fp8")
155+
156+
actual_model = vision_model if vision_model else default_vision_model
157+
actual_temperature = temperature if temperature is not None else 0.3
158+
159+
# Provider logic:
160+
# - If user specifies a custom vision model, clear provider
161+
# - Unless user also explicitly specifies a provider
162+
if provider is not None:
163+
actual_provider = provider if provider else None
164+
elif vision_model is not None:
165+
# Custom vision model → clear provider
166+
actual_provider = None
167+
else:
168+
actual_provider = default_vision_provider
116169

117170
vision_llm = create_openrouter_llm(
118-
model=vision_model,
171+
model=actual_model,
119172
api_key=openrouter_key,
120-
temperature=0.3,
121-
provider=vision_provider,
173+
temperature=actual_temperature,
174+
provider=actual_provider,
122175
)
123176

124177
return VisionAgent(llm=vision_llm)
@@ -365,6 +418,10 @@ def get_default_path(docker_path: str, local_path: str) -> str:
365418
"X-Requested-With",
366419
"X-API-Key",
367420
"X-OpenRouter-Key", # BYOK mode
421+
"X-OpenRouter-Model", # BYOK model override
422+
"X-OpenRouter-Vision-Model", # BYOK vision model override
423+
"X-OpenRouter-Provider", # BYOK provider preference
424+
"X-OpenRouter-Temperature", # BYOK temperature override
368425
],
369426
max_age=3600, # Cache preflight requests for 1 hour
370427
)
@@ -447,12 +504,29 @@ async def annotate(
447504
"""
448505
# Determine which workflow to use
449506
if api_key == "byok":
450-
# BYOK mode: Create workflow with user's key
507+
# BYOK mode: Create workflow with user's key and model settings
451508
openrouter_key = req.headers.get("x-openrouter-key")
452509
if not openrouter_key:
453510
raise HTTPException(status_code=401, detail="Missing X-OpenRouter-Key header")
511+
512+
# Get model config: request body > headers > server defaults
513+
model = request.model or req.headers.get("x-openrouter-model")
514+
provider = request.provider or req.headers.get("x-openrouter-provider")
515+
temp_header = req.headers.get("x-openrouter-temperature")
516+
temperature = request.temperature
517+
if temperature is None and temp_header:
518+
try:
519+
temperature = float(temp_header)
520+
except ValueError:
521+
pass # Invalid header value, use default
522+
454523
try:
455-
active_workflow = create_byok_workflow(openrouter_key)
524+
active_workflow = create_byok_workflow(
525+
openrouter_key,
526+
model=model,
527+
provider=provider,
528+
temperature=temperature,
529+
)
456530
except Exception as e:
457531
raise HTTPException(
458532
status_code=500, detail=f"Failed to initialize BYOK workflow: {str(e)}"
@@ -530,13 +604,36 @@ async def annotate_from_image(
530604
"""
531605
# Determine which workflow and vision agent to use
532606
if api_key == "byok":
533-
# BYOK mode: Create workflow and vision agent with user's key
607+
# BYOK mode: Create workflow and vision agent with user's key and model settings
534608
openrouter_key = req.headers.get("x-openrouter-key")
535609
if not openrouter_key:
536610
raise HTTPException(status_code=401, detail="Missing X-OpenRouter-Key header")
611+
612+
# Get model config: request body > headers > server defaults
613+
model = request.model or req.headers.get("x-openrouter-model")
614+
vision_model = request.vision_model or req.headers.get("x-openrouter-vision-model")
615+
provider = request.provider or req.headers.get("x-openrouter-provider")
616+
temp_header = req.headers.get("x-openrouter-temperature")
617+
temperature = request.temperature
618+
if temperature is None and temp_header:
619+
try:
620+
temperature = float(temp_header)
621+
except ValueError:
622+
pass # Invalid header value, use default
623+
537624
try:
538-
active_workflow = create_byok_workflow(openrouter_key)
539-
active_vision_agent = create_byok_vision_agent(openrouter_key)
625+
active_workflow = create_byok_workflow(
626+
openrouter_key,
627+
model=model,
628+
provider=provider,
629+
temperature=temperature,
630+
)
631+
active_vision_agent = create_byok_vision_agent(
632+
openrouter_key,
633+
vision_model=vision_model,
634+
provider=provider,
635+
temperature=temperature,
636+
)
540637
except Exception as e:
541638
raise HTTPException(
542639
status_code=500, detail=f"Failed to initialize BYOK agents: {str(e)}"

0 commit comments

Comments
 (0)