fetch_models_for_opencode.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. package main
  2. import (
  3. "encoding/json"
  4. "flag"
  5. "fmt"
  6. "io"
  7. "log"
  8. "net/http"
  9. "os"
  10. "strings"
  11. )
  12. type ModelsResponse struct {
  13. Object string `json:"object"`
  14. Data []Model `json:"data"`
  15. }
  16. type Model struct {
  17. ID string `json:"id"`
  18. Object string `json:"object"`
  19. Created int64 `json:"created,omitempty"`
  20. OwnedBy string `json:"owned_by,omitempty"`
  21. Metadata map[string]interface{} `json:"metadata,omitempty"`
  22. }
  23. type Limit struct {
  24. Context int `json:"context"`
  25. Output int `json:"output"`
  26. }
  27. type OpencodeModel struct {
  28. Name string `json:"name"`
  29. Limit Limit `json:"limit"`
  30. }
  31. // V2-native shape: "provider" / "npm" / "options"
  32. type DynamicProviderV2 struct {
  33. Npm string `json:"npm"`
  34. Name string `json:"name"`
  35. Options map[string]string `json:"options"`
  36. Models map[string]OpencodeModel `json:"models,omitempty"`
  37. }
  38. // V1-compatible shape: "providers" / "package" / "settings"
  39. type DynamicProviderV1 struct {
  40. Package string `json:"package"`
  41. Name string `json:"name"`
  42. Settings map[string]string `json:"settings"`
  43. Models map[string]OpencodeModel `json:"models,omitempty"`
  44. }
  45. // OpencodeConfigV2 is the V2-native top-level shape
  46. type OpencodeConfigV2 struct {
  47. Schema string `json:"$schema"`
  48. Provider map[string]DynamicProviderV2 `json:"provider"`
  49. }
  50. // OpencodeConfigV1 is the V1-compatible top-level shape
  51. type OpencodeConfigV1 struct {
  52. Schema string `json:"$schema"`
  53. Providers map[string]DynamicProviderV1 `json:"providers"`
  54. }
  55. const (
  56. formatV2Native = "v2" // default
  57. formatV1Compat = "v1"
  58. )
  59. func getIntFromMetadata(md map[string]interface{}, key string, defaultVal int) int {
  60. if md == nil {
  61. return defaultVal
  62. }
  63. if v, ok := md[key]; ok {
  64. if i, ok := v.(float64); ok {
  65. return int(i)
  66. }
  67. }
  68. return defaultVal
  69. }
  70. func buildConfig(providerName, baseURL, token string, format string, data []Model) ([]byte, error) {
  71. schema := "https://opencode.ai/config.json"
  72. switch format {
  73. case formatV2Native:
  74. return buildV2Config(schema, providerName, baseURL, token, data)
  75. case formatV1Compat:
  76. return buildV1Config(schema, providerName, baseURL, token, data)
  77. default:
  78. return nil, fmt.Errorf("unsupported format: %s", format)
  79. }
  80. }
  81. func buildV2Config(schema, providerName, baseURL, token string, data []Model) ([]byte, error) {
  82. provider := DynamicProviderV2{
  83. Npm: "@ai-sdk/openai-compatible",
  84. Name: providerName,
  85. Options: map[string]string{
  86. "baseURL": baseURL,
  87. "apiKey": token,
  88. },
  89. Models: make(map[string]OpencodeModel, len(data)),
  90. }
  91. for _, m := range data {
  92. ctx := getIntFromMetadata(m.Metadata, "context_window", 131072)
  93. out := getIntFromMetadata(m.Metadata, "max_output_tokens", 8192)
  94. provider.Models[m.ID] = OpencodeModel{
  95. Name: m.ID,
  96. Limit: Limit{
  97. Context: ctx,
  98. Output: out,
  99. },
  100. }
  101. }
  102. config := OpencodeConfigV2{
  103. Schema: schema,
  104. Provider: map[string]DynamicProviderV2{providerName: provider},
  105. }
  106. return json.MarshalIndent(config, "", " ")
  107. }
  108. func buildV1Config(schema, providerName, baseURL, token string, data []Model) ([]byte, error) {
  109. provider := DynamicProviderV1{
  110. Package: "@ai-sdk/openai-compatible",
  111. Name: providerName,
  112. Settings: map[string]string{"baseURL": baseURL, "apiKey": token},
  113. Models: make(map[string]OpencodeModel, len(data)),
  114. }
  115. for _, m := range data {
  116. ctx := getIntFromMetadata(m.Metadata, "context_window", 131072)
  117. out := getIntFromMetadata(m.Metadata, "max_output_tokens", 8192)
  118. provider.Models[m.ID] = OpencodeModel{
  119. Name: m.ID,
  120. Limit: Limit{
  121. Context: ctx,
  122. Output: out,
  123. },
  124. }
  125. }
  126. config := OpencodeConfigV1{
  127. Schema: schema,
  128. Providers: map[string]DynamicProviderV1{providerName: provider},
  129. }
  130. return json.MarshalIndent(config, "", " ")
  131. }
  132. func main() {
  133. var providerName, baseURL, apiURL, outputFile, token, format string
  134. flag.StringVar(&providerName, "provider", "Myprovider", "Name of the provider (default: Myprovider)")
  135. flag.StringVar(&baseURL, "baseurl", "", "Base URL for the provider (required, e.g., http://localhost:8080/v1)")
  136. flag.StringVar(&apiURL, "apiurl", "", "API URL to fetch models from (default: <baseurl>/models)")
  137. flag.StringVar(&token, "token", "", "API token sent as Authorization: Bearer and x-api-key (optional)")
  138. flag.StringVar(&outputFile, "output", "opencode.jsonc", "Output file name (default: opencode.jsonc)")
  139. flag.StringVar(&format, "format", formatV2Native, "Output format: v2 (default, V2-native: provider/npm/options) or v1 (V1-compatible: providers/package/settings)")
  140. flag.Usage = func() {
  141. fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0])
  142. fmt.Fprintf(os.Stderr, "Examples:\n")
  143. fmt.Fprintf(os.Stderr, " %s --provider Myprovider --baseurl http://localhost:8080/v1\n", os.Args[0])
  144. fmt.Fprintf(os.Stderr, " %s --provider OmniRoute --baseurl http://localhost:20128/v1 --format v1\n\n", os.Args[0])
  145. flag.PrintDefaults()
  146. }
  147. flag.Parse()
  148. if baseURL == "" {
  149. log.Fatal("--baseurl is required")
  150. }
  151. if apiURL == "" {
  152. apiURL = strings.TrimSuffix(baseURL, "/") + "/models"
  153. }
  154. // Validate format
  155. switch format {
  156. case formatV2Native, formatV1Compat:
  157. default:
  158. log.Fatalf("Invalid --format %q: must be %q or %q", format, formatV2Native, formatV1Compat)
  159. }
  160. req, err := http.NewRequest("GET", apiURL, nil)
  161. if err != nil {
  162. log.Fatalf("Failed to create request: %v", err)
  163. }
  164. if token != "" {
  165. req.Header.Set("Authorization", "Bearer "+token)
  166. req.Header.Set("x-api-key", token)
  167. }
  168. resp, err := http.DefaultClient.Do(req)
  169. if err != nil {
  170. log.Fatalf("Failed to fetch %s: %v", apiURL, err)
  171. }
  172. defer resp.Body.Close()
  173. if resp.StatusCode != http.StatusOK {
  174. log.Fatalf("Unexpected status code: %d", resp.StatusCode)
  175. }
  176. body, err := io.ReadAll(resp.Body)
  177. if err != nil {
  178. log.Fatalf("Failed to read response: %v", err)
  179. }
  180. var modelsResp ModelsResponse
  181. if err := json.Unmarshal(body, &modelsResp); err != nil {
  182. log.Fatalf("Failed to parse models response: %v", err)
  183. }
  184. out, err := buildConfig(providerName, baseURL, token, format, modelsResp.Data)
  185. if err != nil {
  186. log.Fatalf("Failed to build config: %v", err)
  187. }
  188. if err := os.WriteFile(outputFile, out, 0644); err != nil {
  189. log.Fatalf("Failed to write %s: %v", outputFile, err)
  190. }
  191. fmt.Printf("Successfully wrote %s with %d models for provider %s (format: %s)\n",
  192. outputFile, len(modelsResp.Data), providerName, format)
  193. }