While Pi's ~/.pi/agent/settings.json has a non-empty enabledModels (its Ctrl+P / /scoped-models list), a new session starts on defaultProvider/defaultModel only when that list takes the model in, else on the list's first entry, so a model picked in magpie outside the list was written and never used. Setting Pi's model now adds it to enabledModels as provider/model, as Pi's own picker does, when nothing in the list takes it in already: the same reference with or without a :level after it, or a glob (*, ?, [..], without case) that the reference or its model id fits. The user's entries, their order and comments stay; no list is made where there is none, and an empty one is left as it is. Turning the model back to Pi's own takes magpie/… entries out with magpie's provider, removing the key rather than leaving it empty. The model field shows what a new session starts on: the list's first model when the saved default isn't in the list and that entry names one model outright. The list itself isn't editable in magpie yet; Field is single-valued, and an editor needs a multi-value field kind in the API, GUI (a multi-select, reorderable picker), TUI and CLI. Pi's models.json now gives each magpie model the API its provider speaks natively, so the gateway passes the request through instead of translating Chat: Responses-native models keep openai-responses; a Messages-only provider's (a custom Anthropic-compatible endpoint, as Cleary's) get api anthropic-messages with baseUrl the gateway's root (Anthropic's SDK adds /v1/messages). A Claude from 4.6 on also gets compat.forceAdaptiveThinking, so Pi asks for adaptive thinking and an effort, not the budget such a Claude refuses (why these stayed on Chat before). Magpie's models stay under providers.magpie; other providers are untouched. Why Chat→Messages could 400 where Pi's direct Messages didn't: with reasoning on, buildAnthropic raised max_tokens to budget+4096 (28096 for high) past the client's own max_completion_tokens, which Pi sets to the model's output limit, so an upstream capped below that refused it. The budget now fits under a cap the client gives (at most the cap less 1024; no thinking when the cap is 2048 or less); with no cap it still raises max_tokens as before. Tested: go test -tags nogui ./... (new: TestPiTakes, TestPiModelJoinsEnabledModels, TestPiModelOffLeavesEnabledModels, TestThinkingBudgetFitsTheClientsCap; TestPiModelsAskedOnTheirNativeAPI extended), nogui builds for darwin, linux and windows. In a sandbox HOME: settings.json with enabledModels [xai/grok-4.6, openai-codex/gpt-5.6-sol, deepseek/deepseek-flash], magpie pi magpie/cleary/kimi-k3 appended the model and kept the rest, magpie pi default took it out again; models.json listed cleary's models on anthropic-messages, a Chat relay's on the provider's openai-completions, a Responses vendor's on openai-responses. Pi 0.87.1 loaded that models.json (--list-models) and, against a fake Messages endpoint standing in for the gateway (MAGPIE_ADDR on a spare port), posted to /v1/messages with the gateway key: kimi-k3 with a 15360-token budget under max_tokens 16384, claude-sonnet-5 with adaptive thinking and effort high.
77 lines
2.8 KiB
Go
77 lines
2.8 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/yetone/magpie/internal/gateway"
|
|
"github.com/yetone/magpie/internal/provider"
|
|
)
|
|
|
|
// Pi asks each model on the API its provider serves it on, so the gateway
|
|
// relays the request as it is: a Responses-only provider's models and GPT
|
|
// on OpenAI's API go on openai-responses; a Messages-only provider's on
|
|
// anthropic-messages at the gateway; a Chat provider's and a group's stay
|
|
// on the provider's openai-completions.
|
|
func TestPiModelsAskedOnTheirNativeAPI(t *testing.T) {
|
|
home := syncHome(t)
|
|
for _, p := range []provider.Provider{
|
|
{ID: "resp", Name: "Resp", Key: "k", Responses: "http://127.0.0.1:1/v1", Models: []string{"grok-5", "gpt-5.5"}},
|
|
{ID: "openai", Name: "OpenAI", Key: "k", Chat: "https://api.openai.com/v1", Responses: "https://api.openai.com/v1", Models: []string{"gpt-5.5", "whisper-x"}},
|
|
{ID: "anth", Name: "Anth", Key: "k", Anthropic: "http://127.0.0.1:1", Models: []string{"claude-sonnet-5", "kimi-k3"}},
|
|
} {
|
|
if err := provider.Save(p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := pi(home).Field("model").Set("magpie/resp/grok-5"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var file map[string]any
|
|
if err := json.Unmarshal([]byte(readFile(filepath.Join(home, ".pi", "agent", "models.json"))), &file); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pm := file["providers"].(map[string]any)["magpie"].(map[string]any)
|
|
if pm["api"] != "openai-completions" || pm["baseUrl"] != gatewayV1() {
|
|
t.Fatalf("provider: %v", pm)
|
|
}
|
|
apis, entries := map[string]any{}, map[string]map[string]any{}
|
|
for _, raw := range pm["models"].([]any) {
|
|
m := raw.(map[string]any)
|
|
apis[m["id"].(string)], entries[m["id"].(string)] = m["api"], m
|
|
}
|
|
for id, want := range map[string]any{
|
|
"resp/grok-5": "openai-responses",
|
|
"resp/gpt-5.5": "openai-responses",
|
|
"openai/gpt-5.5": "openai-responses",
|
|
"openai/whisper-x": nil,
|
|
"relay/glm-4.6": nil,
|
|
"anth/claude-sonnet-5": "anthropic-messages",
|
|
"anth/kimi-k3": "anthropic-messages",
|
|
"group/auto-gpt-5-5": nil,
|
|
} {
|
|
got, ok := apis[id]
|
|
if !ok {
|
|
t.Errorf("%s not listed: %v", id, apis)
|
|
} else if got != want {
|
|
t.Errorf("%s: api %v, want %v", id, got, want)
|
|
}
|
|
}
|
|
// Anthropic's SDK posts to baseUrl + /v1/messages; only the Claude
|
|
// that refuses a thinking budget is told to think adaptively
|
|
for id, adaptive := range map[string]bool{"anth/claude-sonnet-5": true, "anth/kimi-k3": false} {
|
|
e := entries[id]
|
|
if e["baseUrl"] != gateway.URL() {
|
|
t.Errorf("%s: baseUrl %v, want %s", id, e["baseUrl"], gateway.URL())
|
|
}
|
|
c, _ := e["compat"].(map[string]any)
|
|
if got := c["forceAdaptiveThinking"] == true; got != adaptive {
|
|
t.Errorf("%s: compat %v", id, e["compat"])
|
|
}
|
|
}
|
|
if _, ok := entries["resp/grok-5"]["baseUrl"]; ok {
|
|
t.Errorf("a Responses model keeps the provider's baseUrl: %v", entries["resp/grok-5"])
|
|
}
|
|
}
|