package main import ( "encoding/json" "flag" "fmt" "io" "log" "net/http" "os" "strings" ) type ModelsResponse struct { Object string `json:"object"` Data []Model `json:"data"` } type Model struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created,omitempty"` OwnedBy string `json:"owned_by,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` } type Limit struct { Context int `json:"context"` Output int `json:"output"` } type OpencodeModel struct { Name string `json:"name"` Limit Limit `json:"limit"` } // V2-native shape: "provider" / "npm" / "options" type DynamicProviderV2 struct { Npm string `json:"npm"` Name string `json:"name"` Options map[string]string `json:"options"` Models map[string]OpencodeModel `json:"models,omitempty"` } // V1-compatible shape: "providers" / "package" / "settings" type DynamicProviderV1 struct { Package string `json:"package"` Name string `json:"name"` Settings map[string]string `json:"settings"` Models map[string]OpencodeModel `json:"models,omitempty"` } // OpencodeConfigV2 is the V2-native top-level shape type OpencodeConfigV2 struct { Schema string `json:"$schema"` Provider map[string]DynamicProviderV2 `json:"provider"` } // OpencodeConfigV1 is the V1-compatible top-level shape type OpencodeConfigV1 struct { Schema string `json:"$schema"` Providers map[string]DynamicProviderV1 `json:"providers"` } const ( formatV2Native = "v2" // default formatV1Compat = "v1" ) func getIntFromMetadata(md map[string]interface{}, key string, defaultVal int) int { if md == nil { return defaultVal } if v, ok := md[key]; ok { if i, ok := v.(float64); ok { return int(i) } } return defaultVal } func buildConfig(providerName, baseURL, token string, format string, data []Model) ([]byte, error) { schema := "https://opencode.ai/config.json" switch format { case formatV2Native: return buildV2Config(schema, providerName, baseURL, token, data) case formatV1Compat: return buildV1Config(schema, providerName, baseURL, token, data) default: return nil, fmt.Errorf("unsupported format: %s", format) } } func buildV2Config(schema, providerName, baseURL, token string, data []Model) ([]byte, error) { provider := DynamicProviderV2{ Npm: "@ai-sdk/openai-compatible", Name: providerName, Options: map[string]string{ "baseURL": baseURL, "apiKey": token, }, Models: make(map[string]OpencodeModel, len(data)), } for _, m := range data { ctx := getIntFromMetadata(m.Metadata, "context_window", 131072) out := getIntFromMetadata(m.Metadata, "max_output_tokens", 8192) provider.Models[m.ID] = OpencodeModel{ Name: m.ID, Limit: Limit{ Context: ctx, Output: out, }, } } config := OpencodeConfigV2{ Schema: schema, Provider: map[string]DynamicProviderV2{providerName: provider}, } return json.MarshalIndent(config, "", " ") } func buildV1Config(schema, providerName, baseURL, token string, data []Model) ([]byte, error) { provider := DynamicProviderV1{ Package: "@ai-sdk/openai-compatible", Name: providerName, Settings: map[string]string{"baseURL": baseURL, "apiKey": token}, Models: make(map[string]OpencodeModel, len(data)), } for _, m := range data { ctx := getIntFromMetadata(m.Metadata, "context_window", 131072) out := getIntFromMetadata(m.Metadata, "max_output_tokens", 8192) provider.Models[m.ID] = OpencodeModel{ Name: m.ID, Limit: Limit{ Context: ctx, Output: out, }, } } config := OpencodeConfigV1{ Schema: schema, Providers: map[string]DynamicProviderV1{providerName: provider}, } return json.MarshalIndent(config, "", " ") } func main() { var providerName, baseURL, apiURL, outputFile, token, format string flag.StringVar(&providerName, "provider", "Myprovider", "Name of the provider (default: Myprovider)") flag.StringVar(&baseURL, "baseurl", "", "Base URL for the provider (required, e.g., http://localhost:8080/v1)") flag.StringVar(&apiURL, "apiurl", "", "API URL to fetch models from (default: /models)") flag.StringVar(&token, "token", "", "API token sent as Authorization: Bearer and x-api-key (optional)") flag.StringVar(&outputFile, "output", "opencode.jsonc", "Output file name (default: opencode.jsonc)") flag.StringVar(&format, "format", formatV2Native, "Output format: v2 (default, V2-native: provider/npm/options) or v1 (V1-compatible: providers/package/settings)") flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Examples:\n") fmt.Fprintf(os.Stderr, " %s --provider Myprovider --baseurl http://localhost:8080/v1\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s --provider OmniRoute --baseurl http://localhost:20128/v1 --format v1\n\n", os.Args[0]) flag.PrintDefaults() } flag.Parse() if baseURL == "" { log.Fatal("--baseurl is required") } if apiURL == "" { apiURL = strings.TrimSuffix(baseURL, "/") + "/models" } // Validate format switch format { case formatV2Native, formatV1Compat: default: log.Fatalf("Invalid --format %q: must be %q or %q", format, formatV2Native, formatV1Compat) } req, err := http.NewRequest("GET", apiURL, nil) if err != nil { log.Fatalf("Failed to create request: %v", err) } if token != "" { req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("x-api-key", token) } resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatalf("Failed to fetch %s: %v", apiURL, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Fatalf("Unexpected status code: %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { log.Fatalf("Failed to read response: %v", err) } var modelsResp ModelsResponse if err := json.Unmarshal(body, &modelsResp); err != nil { log.Fatalf("Failed to parse models response: %v", err) } out, err := buildConfig(providerName, baseURL, token, format, modelsResp.Data) if err != nil { log.Fatalf("Failed to build config: %v", err) } if err := os.WriteFile(outputFile, out, 0644); err != nil { log.Fatalf("Failed to write %s: %v", outputFile, err) } fmt.Printf("Successfully wrote %s with %d models for provider %s (format: %s)\n", outputFile, len(modelsResp.Data), providerName, format) }