-
Notifications
You must be signed in to change notification settings - Fork 360
feat(api): accept model overrides on session creation and add runtime model switching endpoints #2791
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(api): accept model overrides on session creation and add runtime model switching endpoints #2791
Changes from all commits
2309007
e324ead
3ed8b94
80d75e5
c0e6fb9
e38635f
d90661b
90c7b4c
baf326c
49d1b09
da48ca8
6d1457c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,49 +16,136 @@ import ( | |
| "github.com/docker/docker-agent/pkg/modelsdev" | ||
| ) | ||
|
|
||
| // ModelChoice represents a model available for selection in the TUI picker. | ||
| // ModelChoice represents a model available for selection in the model picker. | ||
| // | ||
| // JSON tags are part of the public wire format used by | ||
| // GET /api/sessions/:id/models; renaming a tag is a breaking change. | ||
| type ModelChoice struct { | ||
| // Name is the display name (config key) | ||
| Name string | ||
| Name string `json:"name"` | ||
| // Ref is the model reference used internally (e.g., "my_model" or "openai/gpt-4o") | ||
| Ref string | ||
| Ref string `json:"ref"` | ||
| // Provider is the provider name (e.g., "openai", "anthropic") | ||
| Provider string | ||
| Provider string `json:"provider,omitempty"` | ||
| // Model is the specific model name (e.g., "gpt-4o", "claude-sonnet-4-0") | ||
| Model string | ||
| Model string `json:"model,omitempty"` | ||
| // IsDefault indicates this is the agent's configured default model | ||
| IsDefault bool | ||
| IsDefault bool `json:"is_default,omitempty"` | ||
| // IsCurrent indicates this is the currently active model for the agent | ||
| IsCurrent bool | ||
| IsCurrent bool `json:"is_current,omitempty"` | ||
| // IsCustom indicates this is a custom model from the session history (not from config) | ||
| IsCustom bool | ||
| IsCustom bool `json:"is_custom,omitempty"` | ||
| // IsCatalog indicates this is a model from the models.dev catalog | ||
| IsCatalog bool | ||
| IsCatalog bool `json:"is_catalog,omitempty"` | ||
|
|
||
| // The fields below are populated (best-effort) from the models.dev | ||
| // catalog. They are optional and may all be zero/empty when no | ||
| // catalog entry is found for the model. | ||
|
|
||
| // Family is the model family (e.g., "claude", "gpt"). | ||
| Family string | ||
| Family string `json:"family,omitempty"` | ||
| // InputCost is the price (in USD) per 1M input tokens. | ||
| InputCost float64 | ||
| InputCost float64 `json:"input_cost,omitempty"` | ||
| // OutputCost is the price (in USD) per 1M output tokens. | ||
| OutputCost float64 | ||
| OutputCost float64 `json:"output_cost,omitempty"` | ||
| // CacheReadCost is the price (in USD) per 1M cached input tokens. | ||
| CacheReadCost float64 | ||
| CacheReadCost float64 `json:"cache_read_cost,omitempty"` | ||
| // CacheWriteCost is the price (in USD) per 1M cache-write tokens. | ||
| CacheWriteCost float64 | ||
| CacheWriteCost float64 `json:"cache_write_cost,omitempty"` | ||
| // ContextLimit is the maximum context window size in tokens. | ||
| ContextLimit int | ||
| ContextLimit int `json:"context_limit,omitempty"` | ||
| // OutputLimit is the maximum number of tokens the model can produce | ||
| // in a single response. | ||
| OutputLimit int64 | ||
| OutputLimit int64 `json:"output_limit,omitempty"` | ||
| // InputModalities lists the input modalities supported by the model | ||
| // (e.g., "text", "image", "audio"). | ||
| InputModalities []string | ||
| InputModalities []string `json:"input_modalities,omitempty"` | ||
| // OutputModalities lists the output modalities the model can produce. | ||
| OutputModalities []string | ||
| OutputModalities []string `json:"output_modalities,omitempty"` | ||
| } | ||
|
|
||
|
dgageot marked this conversation as resolved.
|
||
| // SessionModelsResponse is the response returned by | ||
| // GET /api/sessions/:id/models. CurrentModelRef is the active override for | ||
| // the named agent (empty when the agent is using its configured default). | ||
| type SessionModelsResponse struct { | ||
| Agent string `json:"agent"` | ||
| CurrentModelRef string `json:"current_model_ref,omitempty"` | ||
| Models []ModelChoice `json:"models"` | ||
|
dgageot marked this conversation as resolved.
|
||
| } | ||
|
Comment on lines
+67
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question — |
||
|
|
||
| // DecorateModelChoices marks the active selection with IsCurrent and | ||
| // appends any custom (provider/model) refs from the session history that | ||
| // the runtime does not already expose. It is used by every consumer that | ||
| // wants to render a model picker (the TUI App, the HTTP /sessions/:id/models | ||
| // endpoint, …) so they all agree on which entry is current and what the | ||
| // final list looks like. | ||
| // | ||
| // currentRef is the model override active for the agent ("" when none), | ||
| // and customRefs is the session's CustomModelsUsed history. | ||
| // | ||
| // The input slice is never mutated: callers can safely pass a slice that | ||
| // is shared with or backed by an internal cache. | ||
| func DecorateModelChoices(models []ModelChoice, currentRef string, customRefs []string) []ModelChoice { | ||
| // Defensive copy: AvailableModels implementations may return a slice | ||
| // backed by an internal cache. Mutating its IsCurrent flag in place | ||
| // would leak picker state across sessions/agents. | ||
| result := make([]ModelChoice, len(models), len(models)+len(customRefs)+1) | ||
| copy(result, models) | ||
|
|
||
| existingRefs := make(map[string]bool, len(result)) | ||
| for _, m := range result { | ||
| existingRefs[m.Ref] = true | ||
| } | ||
|
|
||
| currentFound := currentRef == "" | ||
| for i := range result { | ||
| if currentRef != "" { | ||
| if result[i].Ref == currentRef { | ||
| result[i].IsCurrent = true | ||
| currentFound = true | ||
| } | ||
| } else { | ||
| result[i].IsCurrent = result[i].IsDefault | ||
| } | ||
| } | ||
|
|
||
| for _, ref := range customRefs { | ||
| if existingRefs[ref] { | ||
| continue | ||
| } | ||
| existingRefs[ref] = true | ||
|
|
||
| prov, name, _ := strings.Cut(ref, "/") | ||
| isCurrent := ref == currentRef | ||
| if isCurrent { | ||
| currentFound = true | ||
| } | ||
| result = append(result, ModelChoice{ | ||
| Name: ref, | ||
| Ref: ref, | ||
| Provider: prov, | ||
| Model: name, | ||
| IsCurrent: isCurrent, | ||
| IsCustom: true, | ||
| }) | ||
| } | ||
|
|
||
| // If the override points at an inline provider/model not in the | ||
| // runtime's list nor in the session's history, fabricate a synthetic | ||
| // choice so the picker can still highlight the active selection. | ||
| if !currentFound && strings.Contains(currentRef, "/") { | ||
| prov, name, _ := strings.Cut(currentRef, "/") | ||
| result = append(result, ModelChoice{ | ||
| Name: currentRef, | ||
| Ref: currentRef, | ||
| Provider: prov, | ||
| Model: name, | ||
| IsCurrent: true, | ||
| IsCustom: true, | ||
|
dgageot marked this conversation as resolved.
|
||
| }) | ||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| // ModelSwitcherConfig holds the configuration needed for model switching. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.