cmd/sketch: add skaband-supported qwen
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: scbc3e984d79ceda6k
diff --git a/cmd/sketch/main.go b/cmd/sketch/main.go
index c4c978e..36f1d1b 100644
--- a/cmd/sketch/main.go
+++ b/cmd/sketch/main.go
@@ -127,6 +127,7 @@
fmt.Println("- opus (Claude 4 Opus)")
fmt.Println("- sonnet (Claude 4 Sonnet)")
fmt.Println("- gemini (Google Gemini 2.5 Pro)")
+ fmt.Println("- qwen (Qwen3-Coder)")
for _, name := range oai.ListModels() {
note := ""
if name != "gpt4.1" {
@@ -141,10 +142,10 @@
return dumpDistFilesystem(flagArgs.dumpDist)
}
- // Only Claude and Gemini have skaband support, for now.
- hasSkabandSupport := flagArgs.modelName == "gemini" || ant.IsClaudeModel(flagArgs.modelName)
+ // Only Claude, Gemini, and Qwen have skaband support, for now.
+ hasSkabandSupport := flagArgs.modelName == "gemini" || ant.IsClaudeModel(flagArgs.modelName) || flagArgs.modelName == "qwen"
if !hasSkabandSupport && flagArgs.skabandAddr != "" {
- return fmt.Errorf("only claude and gemini are supported by skaband, use -skaband-addr='' for other models")
+ return fmt.Errorf("only claude, gemini, and qwen are supported by skaband, use -skaband-addr='' for other models")
}
if err := flagArgs.experimentFlag.Process(); err != nil {
@@ -529,6 +530,7 @@
SkabandAddr: flags.skabandAddr,
Model: flags.modelName,
ModelURL: spec.modelURL,
+ OAIModelName: spec.oaiModelName,
ModelAPIKey: spec.apiKey,
Path: cwd,
GitUsername: flags.gitUsername,
@@ -583,8 +585,12 @@
if err != nil && os.Getenv("SKETCH_MODEL_URL") != "" {
return err
}
-
- return setupAndRunAgent(ctx, flags, modelURL, apiKey, pubKey, true, logFile)
+ spec := modelSpec{
+ modelURL: modelURL,
+ oaiModelName: os.Getenv("SKETCH_OAI_MODEL_NAME"),
+ apiKey: apiKey,
+ }
+ return setupAndRunAgent(ctx, flags, spec, pubKey, true, logFile)
}
// runInUnsafeMode handles execution on the host machine without Docker.
@@ -594,12 +600,13 @@
if err != nil {
return err
}
- return setupAndRunAgent(ctx, flags, spec.modelURL, spec.apiKey, pubKey, false, logFile)
+ return setupAndRunAgent(ctx, flags, spec, pubKey, false, logFile)
}
type modelSpec struct {
- modelURL string
- apiKey string
+ modelURL string
+ oaiModelName string // the OpenAI model name, if applicable; this varies even for the same model by provider
+ apiKey string
}
// resolveModel logs in to skaband (as appropriate) and resolves the flags to a model URL and API key.
@@ -608,7 +615,7 @@
if err != nil {
return modelSpec{}, "", err
}
- pubKey, modelURL, apiKey, err := skabandclient.Login(os.Stdout, privKey, flags.skabandAddr, flags.sessionID, flags.modelName)
+ pubKey, modelURL, oaiModelName, apiKey, err := skabandclient.Login(os.Stdout, privKey, flags.skabandAddr, flags.sessionID, flags.modelName)
if err != nil {
return modelSpec{}, "", err
}
@@ -625,12 +632,12 @@
}
}
- return modelSpec{modelURL: modelURL, apiKey: apiKey}, pubKey, nil
+ return modelSpec{modelURL: modelURL, oaiModelName: oaiModelName, apiKey: apiKey}, pubKey, nil
}
// setupAndRunAgent handles the common logic for setting up and running the agent
// in both container and unsafe modes.
-func setupAndRunAgent(ctx context.Context, flags CLIFlags, modelURL, apiKey, pubKey string, inInsideSketch bool, logFile *os.File) error {
+func setupAndRunAgent(ctx context.Context, flags CLIFlags, spec modelSpec, pubKey string, inInsideSketch bool, logFile *os.File) error {
// Kick off a version/upgrade check early.
// If the results come back quickly enough,
// we can show them as part of the startup UI.
@@ -644,7 +651,7 @@
// This is needed for MCP server authentication placeholder replacement
if pubKey != "" {
os.Setenv("SKETCH_PUB_KEY", pubKey)
- os.Setenv("SKETCH_MODEL_API_KEY", apiKey)
+ os.Setenv("SKETCH_MODEL_API_KEY", spec.apiKey)
}
wd, err := os.Getwd()
@@ -661,7 +668,7 @@
}
}
- llmService, err := selectLLMService(nil, flags, modelURL, apiKey)
+ llmService, err := selectLLMService(nil, flags, spec)
if err != nil {
return fmt.Errorf("failed to initialize LLM service: %w", err)
}
@@ -823,7 +830,7 @@
}
}
if agentConfig.SkabandClient != nil {
- sessionSecret := apiKey
+ sessionSecret := spec.apiKey
go agentConfig.SkabandClient.DialAndServeLoop(ctx, flags.sessionID, sessionSecret, srv, connectFn)
}
}
@@ -930,29 +937,29 @@
// If modelName is "gemini", it uses the Gemini service.
// Otherwise, it tries to use the OpenAI service with the specified model.
// Returns an error if the model name is not recognized or if required configuration is missing.
-func selectLLMService(client *http.Client, flags CLIFlags, modelURL, apiKey string) (llm.Service, error) {
+func selectLLMService(client *http.Client, flags CLIFlags, spec modelSpec) (llm.Service, error) {
if ant.IsClaudeModel(flags.modelName) {
- if apiKey == "" {
+ if spec.apiKey == "" {
return nil, fmt.Errorf("no anthropic api key provided, set %s", ant.APIKeyEnv)
}
return &ant.Service{
HTTPC: client,
- URL: modelURL,
- APIKey: apiKey,
+ URL: spec.modelURL,
+ APIKey: spec.apiKey,
DumpLLM: flags.dumpLLM,
Model: ant.ClaudeModelName(flags.modelName),
}, nil
}
if flags.modelName == "gemini" {
- if apiKey == "" {
+ if spec.apiKey == "" {
return nil, fmt.Errorf("no gemini api key provided, set %s", gem.GeminiAPIKeyEnv)
}
return &gem.Service{
HTTPC: client,
- URL: modelURL,
+ URL: spec.modelURL,
Model: gem.DefaultModel,
- APIKey: apiKey,
+ APIKey: spec.apiKey,
DumpLLM: flags.dumpLLM,
}, nil
}
@@ -963,16 +970,22 @@
}
// Verify we have an API key, if necessary.
- apiKey = cmp.Or(os.Getenv(model.APIKeyEnv), flags.llmAPIKey)
+ apiKey := cmp.Or(spec.apiKey, os.Getenv(model.APIKeyEnv), flags.llmAPIKey)
if apiKey == "" {
return nil, fmt.Errorf("missing API key for %s model, set %s environment variable", model.UserName, model.APIKeyEnv)
}
+ // Respect skaband-provided model name, if present.
+ if spec.oaiModelName != "" {
+ model.ModelName = spec.oaiModelName
+ }
+
return &oai.Service{
- HTTPC: client,
- Model: model,
- APIKey: apiKey,
- DumpLLM: flags.dumpLLM,
+ HTTPC: client,
+ Model: model,
+ ModelURL: spec.modelURL,
+ APIKey: apiKey,
+ DumpLLM: flags.dumpLLM,
}, nil
}
diff --git a/cmd/sketch/main_test.go b/cmd/sketch/main_test.go
index 10ca1e0..c377542 100644
--- a/cmd/sketch/main_test.go
+++ b/cmd/sketch/main_test.go
@@ -65,7 +65,7 @@
}
// This should fail due to missing API key, but should still set the environment variable
- err := setupAndRunAgent(context.TODO(), flags, "", "", testPubKey, false, nil)
+ err := setupAndRunAgent(context.TODO(), flags, modelSpec{}, testPubKey, false, nil)
// Check that the environment variable was set correctly
if os.Getenv("SKETCH_PUB_KEY") != testPubKey {
@@ -98,7 +98,7 @@
}
// This should fail due to missing API key, but should not change the environment variable
- err := setupAndRunAgent(context.TODO(), flags, "", "", "", false, nil)
+ err := setupAndRunAgent(context.TODO(), flags, modelSpec{}, "", false, nil)
// Check that the environment variable was not changed
if os.Getenv("SKETCH_PUB_KEY") != "existing-value" {
diff --git a/dockerimg/dockerimg.go b/dockerimg/dockerimg.go
index 535ad65..80830e8 100644
--- a/dockerimg/dockerimg.go
+++ b/dockerimg/dockerimg.go
@@ -46,6 +46,9 @@
// ModelURL is the URL of the LLM service.
ModelURL string
+ // OAIModelName is the openai model name of the LLM model to use.
+ OAIModelName string
+
// ModelAPIKey is the API key for LLM service.
ModelAPIKey string
@@ -550,6 +553,9 @@
if config.ModelURL != "" {
cmdArgs = append(cmdArgs, "-e", "SKETCH_MODEL_URL="+config.ModelURL)
}
+ if config.OAIModelName != "" {
+ cmdArgs = append(cmdArgs, "-e", "SKETCH_OAI_MODEL_NAME="+config.OAIModelName)
+ }
if config.SketchPubKey != "" {
cmdArgs = append(cmdArgs, "-e", "SKETCH_PUB_KEY="+config.SketchPubKey)
}
diff --git a/llm/oai/oai.go b/llm/oai/oai.go
index 8a450c4..c561095 100644
--- a/llm/oai/oai.go
+++ b/llm/oai/oai.go
@@ -215,6 +215,13 @@
URL: FireworksURL,
APIKeyEnv: FireworksAPIKeyEnv,
}
+
+ // Qwen is a skaband-specific model name for Qwen3-Coder
+ // Provider details (URL and APIKeyEnv) are handled by skaband
+ Qwen = Model{
+ UserName: "qwen",
+ ModelName: "qwen", // skaband will map this to the actual provider model
+ }
)
// Service provides chat completions.
@@ -223,6 +230,7 @@
HTTPC *http.Client // defaults to http.DefaultClient if nil
APIKey string // optional, if not set will try to load from env var
Model Model // defaults to DefaultModel if zero value
+ ModelURL string // optional, overrides Model.URL
MaxTokens int // defaults to DefaultMaxTokens if zero
Org string // optional - organization ID
DumpLLM bool // whether to dump request/response text to files for debugging; defaults to false
@@ -255,6 +263,7 @@
MistralMedium,
DevstralSmall,
Qwen3CoderFireworks,
+ Qwen,
}
// ListModels returns a list of all available models with their user-friendly names.
@@ -635,6 +644,8 @@
return 200000 // 200k for O3 models
case "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct":
return 256000 // 256k native context for Qwen3-Coder
+ case "qwen":
+ return 256000 // 256k native context for Qwen3-Coder
default:
// Default for unknown models
return 128000
@@ -649,8 +660,8 @@
// TODO: do this one during Service setup? maybe with a constructor instead?
config := openai.DefaultConfig(s.APIKey)
- if model.URL != "" {
- config.BaseURL = model.URL
+ if modelURLOverride := cmp.Or(s.ModelURL, model.URL); modelURLOverride != "" {
+ config.BaseURL = modelURLOverride
}
if s.Org != "" {
config.OrgID = s.Org
diff --git a/skabandclient/skabandclient.go b/skabandclient/skabandclient.go
index 2560889..e53adc8 100644
--- a/skabandclient/skabandclient.go
+++ b/skabandclient/skabandclient.go
@@ -192,16 +192,16 @@
// Login connects to skaband and authenticates the user.
// If skabandAddr is empty, it returns the public key without contacting a server.
// It is the caller's responsibility to set the API URL and key in this case.
-func Login(stdout io.Writer, privKey ed25519.PrivateKey, skabandAddr, sessionID, model string) (pubKey, apiURL, apiKey string, err error) {
+func Login(stdout io.Writer, privKey ed25519.PrivateKey, skabandAddr, sessionID, model string) (pubKey, apiURL, oaiModelName, apiKey string, err error) {
sig := ed25519.Sign(privKey, []byte(sessionID))
pubKey = hex.EncodeToString(privKey.Public().(ed25519.PublicKey))
if skabandAddr == "" {
- return pubKey, "", "", nil
+ return pubKey, "", "", "", nil
}
req, err := http.NewRequest("POST", skabandAddr+"/authclient", nil)
if err != nil {
- return "", "", "", err
+ return "", "", "", "", err
}
req.Header.Set("Public-Key", pubKey)
req.Header.Set("Session-ID", sessionID)
@@ -209,25 +209,27 @@
req.Header.Set("X-Model", model)
resp, err := http.DefaultClient.Do(req)
if err != nil {
- return "", "", "", fmt.Errorf("skaband login: %w", err)
+ return "", "", "", "", fmt.Errorf("skaband login: %w", err)
}
apiURL = resp.Header.Get("X-API-URL")
apiKey = resp.Header.Get("X-API-Key")
+ oaiModelName = resp.Header.Get("X-OAI-Model")
defer resp.Body.Close()
_, err = io.Copy(stdout, resp.Body)
if err != nil {
- return "", "", "", fmt.Errorf("skaband login: %w", err)
+ return "", "", "", "", fmt.Errorf("skaband login: %w", err)
}
if resp.StatusCode != 200 {
- return "", "", "", fmt.Errorf("skaband login failed: %d", resp.StatusCode)
+ return "", "", "", "", fmt.Errorf("skaband login failed: %d", resp.StatusCode)
}
if apiURL == "" {
- return "", "", "", fmt.Errorf("skaband returned no api url")
+ return "", "", "", "", fmt.Errorf("skaband returned no api url")
}
if apiKey == "" {
- return "", "", "", fmt.Errorf("skaband returned no api key")
+ return "", "", "", "", fmt.Errorf("skaband returned no api key")
}
- return pubKey, apiURL, apiKey, nil
+ fmt.Printf("skaband login successful, API URL: %s, API Key: %s\n", apiURL, apiKey)
+ return pubKey, apiURL, oaiModelName, apiKey, nil
}
func DefaultKeyPath(skabandAddr string) string {