Download internal/upstream/model_catalog.go from a3216/gcli2api: direct link, hf CLI and curl.
- Browser
- Download file 12.6 kB
-
https://huggingface.co/spaces/a3216/gcli2api/resolve/main/internal/upstream/model_catalog.go
- Command line
-
hf download hf://spaces/a3216/gcli2api/internal/upstream/model_catalog.go
-
curl -L -o model_catalog.go https://huggingface.co/spaces/a3216/gcli2api/resolve/main/internal/upstream/model_catalog.go
12.6 kB
| // model_catalog.go context_length / max_output_tokens 四级查找链 + model.json | |
| // 本地缓存(任务书 model-json-dynamic)。 | |
| // | |
| // 查找链(32a3c13 三级 → 四级,上游动态值永远权威不变): | |
| // 1. 上游动态值(ModelInfo.ContextWindow/MaxTokens)——权威,永远压过 model.json | |
| // (即使后者更新:上游才是权威,任务书 §清理); | |
| // 2. 静态种子表(context_catalog.go 的 contextCapFallback,编译期兜底); | |
| // 3. model.json 本地缓存(数据目录,含运行时从 models.dev 补的值 + 仓库种子 | |
| // embed 的初值);损坏 → 降级种子并 WARN 不崩溃(手动维护入口的容错); | |
| // 4. 触发 models.dev 按需拉取(异步,不阻塞本次响应)→ 值写入 model.json → | |
| // 本次先落 1M/省略,下次命中缓存;负缓存 24h。 | |
| // | |
| // model.json 读写并发安全(单 sync.Mutex 全程持锁 + 原子落盘 tmp+rename, | |
| // 多请求同时 miss 同一模型只写一次);文件损坏/不可写均静默降级(种子表→1M)。 | |
| // | |
| // 状态归属:包级 catalogState 单例(与 modelsDev fetcher 同模式,进程一份)。 | |
| // 测试用 resetModelCatalog / loadModelCatalogAt 隔离。 | |
| package upstream | |
| import ( | |
| "embed" | |
| "encoding/json" | |
| "log" | |
| "net/http" | |
| "os" | |
| "path/filepath" | |
| "sync" | |
| "time" | |
| ) | |
| // modelSeedFS 仓库种子版 model.json(context_catalog 静态表 27 值迁移,source=seed)。 | |
| // 只作 model.json 缺失时的初值来源;运行目录的 model.json 一旦存在则以它为准 | |
| // (用户手动维护入口——直接编辑文件,格式容错由 loadModelCatalog 的校验兜底)。 | |
| // | |
| //go:embed model.json | |
| var modelSeedFS embed.FS | |
| // ModelCapEntry model.json 单条目(与静态表同字段口径 + 来源与抓取时间)。 | |
| // Source:seed(仓库种子迁移)/ modelsdev(运行时按需拉取)/ manual(用户手编 | |
| // ——无法区分手编与 seed,手编条目保留其原 source 字符串,语义等同「非拉取」)。 | |
| // Context 必须 >0(零/负条目校验拒绝);MaxOutput 0 = 输出上限未知 → 省略字段。 | |
| type ModelCapEntry struct { | |
| ContextLength int64 `json:"context_length"` | |
| MaxOutputTokens int64 `json:"max_output_tokens,omitempty"` | |
| FetchedAt string `json:"fetched_at,omitempty"` // RFC3339;seed 条目为空 | |
| Source string `json:"source"` | |
| } | |
| // modelCatalog model.json 缓存状态机(包级 catalogState 单例的字段载体)。 | |
| type modelCatalog struct { | |
| mu sync.Mutex | |
| path string // 落盘路径(空 = 禁用持久化:纯内存 + 种子) | |
| entries map[string]ModelCapEntry | |
| loaded bool // entries 已初始化(含损坏降级形态) | |
| } | |
| // catalogState 包级单例:全进程一份 model.json 状态。 | |
| var catalogState = &modelCatalog{} | |
| // initModelCatalogLocked 确保 entries 已加载(持锁调用): | |
| // - 运行目录 model.json 存在且合法 → 全量加载(校验失败的条目剔除并 WARN); | |
| // - 文件不存在 / 整体损坏(非法 JSON)→ 种子 embed 初值(不落盘:保持「用户 | |
| // 尚无缓存」状态,首次拉取成功后再落盘); | |
| // - path 为空(未接线,如测试/工具进程)→ 种子初值。 | |
| func (c *modelCatalog) initLocked() { | |
| if c.loaded { | |
| return | |
| } | |
| c.entries = map[string]ModelCapEntry{} | |
| c.loaded = true | |
| if c.path == "" { | |
| c.loadSeedLocked() | |
| return | |
| } | |
| raw, err := os.ReadFile(c.path) | |
| if err != nil { | |
| // 不存在 / 不可读 → 种子(首次启动形态)。 | |
| c.loadSeedLocked() | |
| return | |
| } | |
| var file map[string]ModelCapEntry | |
| if err := json.Unmarshal(raw, &file); err != nil { | |
| // 整体损坏:降级种子 + WARN 不崩溃(任务书 §手动维护入口容错)。 | |
| log.Printf("WARN: [upstream] model.json 损坏(降级内置种子表): path=%s err=%v", c.path, err) | |
| c.loadSeedLocked() | |
| return | |
| } | |
| for id, e := range file { | |
| if !validCapEntry(e) { | |
| log.Printf("WARN: [upstream] model.json 条目非法剔除: model=%s entry=%+v", id, e) | |
| continue | |
| } | |
| c.entries[id] = e | |
| } | |
| } | |
| // loadSeedLocked 把仓库种子 model.json 灌入 entries(持锁调用)。 | |
| func (c *modelCatalog) loadSeedLocked() { | |
| raw, err := modelSeedFS.ReadFile("model.json") | |
| if err != nil { | |
| // embed 编译期保证存在,理论不可达;防御性兜底走静态表(initLocked 调用方 | |
| // 查找链第 2 级本来就会兜,这里只需保持 entries 为空)。 | |
| return | |
| } | |
| var seed map[string]ModelCapEntry | |
| if err := json.Unmarshal(raw, &seed); err != nil { | |
| return | |
| } | |
| for id, e := range seed { | |
| if validCapEntry(e) { | |
| c.entries[id] = e | |
| } | |
| } | |
| } | |
| // validCapEntry 条目级校验:context 正数 + 输出非负(任务书 §值校验的写入侧)。 | |
| func validCapEntry(e ModelCapEntry) bool { | |
| return e.ContextLength > 0 && e.MaxOutputTokens >= 0 | |
| } | |
| // get 查 model.json 缓存(第 3 级)。返回条目与是否命中。 | |
| // 只读内存,不发网络;加载与网络动作由 ensure/触发侧负责。 | |
| func (c *modelCatalog) get(model string) (ModelCapEntry, bool) { | |
| c.mu.Lock() | |
| defer c.mu.Unlock() | |
| c.initLocked() | |
| e, ok := c.entries[model] | |
| if !ok || !validCapEntry(e) { | |
| return ModelCapEntry{}, false | |
| } | |
| return e, true | |
| } | |
| // put 写一条缓存(第 4 级拉取成功后调用)并落盘。同模型已存在(用户手动维护过) | |
| // 仍覆盖——运行时拉取值采信 models.dev 官方源优先口径,比手编更可信; | |
| // 若不希望覆盖,删掉 model.json 里对应条目即可(加载后手编值只在本次进程生效)。 | |
| func (c *modelCatalog) put(model string, e ModelCapEntry) { | |
| if model == "" || !validCapEntry(e) { | |
| return | |
| } | |
| c.mu.Lock() | |
| defer c.mu.Unlock() | |
| c.initLocked() | |
| c.entries[model] = e | |
| c.saveLocked() | |
| } | |
| // saveLocked 原子落盘(tmp + rename,pool state.json 同模式)。持锁调用。 | |
| // path 为空 / 目录不可写 / 序列化失败 → 静默(内存缓存仍生效,下次进程重拉)。 | |
| func (c *modelCatalog) saveLocked() { | |
| if c.path == "" { | |
| return | |
| } | |
| raw, err := json.MarshalIndent(c.entries, "", " ") | |
| if err != nil { | |
| return | |
| } | |
| if dir := filepath.Dir(c.path); dir != "" { | |
| _ = os.MkdirAll(dir, 0o755) | |
| } | |
| tmp := c.path + ".tmp" | |
| if err := os.WriteFile(tmp, raw, 0o600); err != nil { | |
| log.Printf("WARN: [upstream] model.json 落盘失败(内存缓存仍生效): path=%s err=%v", c.path, err) | |
| return | |
| } | |
| if err := os.Rename(tmp, c.path); err != nil { | |
| log.Printf("WARN: [upstream] model.json 落盘改名失败: path=%s err=%v", c.path, err) | |
| } | |
| } | |
| // ---- 包级 API(查找链 3/4 级 + 接线)---- | |
| // SetModelCatalogPath 接线 model.json 落盘路径(cmd/server 启动时调用, | |
| // 数据目录与 state.json 同风格)。首次调用生效;后续调用在已加载后仅更新路径 | |
| // (不重载——进程内以内存 entries 为准)。 | |
| func SetModelCatalogPath(path string) { | |
| c := catalogState | |
| c.mu.Lock() | |
| defer c.mu.Unlock() | |
| c.path = path | |
| } | |
| // modelCatalogGet 第 3 级:model.json 缓存命中(内含种子初值与损坏降级)。 | |
| func modelCatalogGet(model string) (ModelCapEntry, bool) { | |
| return catalogState.get(model) | |
| } | |
| // modelCatalogPut 第 4 级写入:models.dev 拉到的值落缓存(含落盘)。 | |
| func modelCatalogPut(model string, context, maxOutput int64) { | |
| modelCatalogPutSourced(model, context, maxOutput, "modelsdev") | |
| } | |
| // modelCatalogPutSourced 写入指定来源的条目(测试可注入 fetched_at 检查落盘格式)。 | |
| func modelCatalogPutSourced(model string, context, maxOutput int64, source string) { | |
| catalogState.put(model, ModelCapEntry{ | |
| ContextLength: context, | |
| MaxOutputTokens: maxOutput, | |
| FetchedAt: time.Now().UTC().Format(time.RFC3339), | |
| Source: source, | |
| }) | |
| } | |
| // resetModelCatalog 测试隔离:清空单例状态(entries/path/loaded)。 | |
| func resetModelCatalog() { | |
| c := catalogState | |
| c.mu.Lock() | |
| defer c.mu.Unlock() | |
| c.entries = nil | |
| c.path = "" | |
| c.loaded = false | |
| } | |
| // loadModelCatalogAt 测试接线:指向指定路径后立即触发一次加载(同步,可断言文件 | |
| // 解析行为)。生产路径用 SetModelCatalogPath(惰性首次 get 触发加载)。 | |
| func loadModelCatalogAt(path string) { | |
| SetModelCatalogPath(path) | |
| catalogState.mu.Lock() | |
| defer catalogState.mu.Unlock() | |
| catalogState.initLocked() | |
| } | |
| // ResetLookupChainForTest 跨包测试钩子:清空 model.json 缓存与 models.dev fetcher | |
| // 的全部包级单例状态(含在途拉取冷却——防 server 包测试末尾的异步 goroutine 打 | |
| // 真网、防跨测试缓存污染)。仅测试引用(upstream 包内用 resetModelsDev / | |
| // resetModelCatalog 等价内联)。 | |
| func ResetLookupChainForTest() { | |
| resetModelCatalog() | |
| resetModelsDev() | |
| } | |
| // ---- 四级查找链(对 handler 暴露的入口,签名与 32a3c13 三级版兼容)---- | |
| // ContextWindowListingV4 四级查找链的 context_length 决策: | |
| // 1. remote>0 权威透出(上游动态值永远压过 model.json,任务书 §清理); | |
| // 2. 静态种子表(contextCapFallback); | |
| // 3. model.json 缓存(内含种子初值 / models.dev 运行时补充值); | |
| // 4. 全链 miss 且非负缓存 → 异步触发 models.dev 拉取(本次返回 DefaultContextWindow | |
| // 1M,不阻塞;拉到后写 model.json 供下次命中)。 | |
| func ContextWindowListingV4(model string, remote int64, client *http.Client) int64 { | |
| if remote > 0 { | |
| return remote | |
| } | |
| if model == "" { | |
| return DefaultContextWindow | |
| } | |
| if cap, ok := contextCapFallback[model]; ok && cap.context > 0 { | |
| return cap.context // 第 2 级:静态种子表(编译期兜底,永远可用) | |
| } | |
| if e, ok := modelCatalogGet(model); ok { | |
| return e.ContextLength // 第 3 级:model.json | |
| } | |
| if !modelsDev.negativeFresh(model) { | |
| // 第 4 级触发:先查进程内文档索引(拉过一次即常驻),命中直接入缓存 | |
| // 返回(不等待异步拉取);未命中 → 记负缓存 + 异步拉取(本次先回 1M)。 | |
| if e, ok := modelsDev.lookup(model); ok { | |
| modelCatalogPut(model, e.Context, e.Output) | |
| return e.Context | |
| } | |
| modelsDev.ensureDocAsync(client, "") | |
| } | |
| return DefaultContextWindow // 第 4 级兜底:1M(本次先回,拉到后下次命中) | |
| } | |
| // MaxOutputTokensListingV4 四级查找链的 max_output_tokens 决策(与 context 口径 | |
| // 刻意不同:未知 → 省略,无「宁可高估」安全侧)。 | |
| func MaxOutputTokensListingV4(model string, remote int64, client *http.Client) (int64, bool) { | |
| if remote > 0 { | |
| return remote, true | |
| } | |
| if model == "" { | |
| return 0, false | |
| } | |
| if cap, ok := contextCapFallback[model]; ok && cap.maxOutput > 0 { | |
| return cap.maxOutput, true // 第 2 级 | |
| } | |
| if e, ok := modelCatalogGet(model); ok && e.MaxOutputTokens > 0 { | |
| return e.MaxOutputTokens, true // 第 3 级 | |
| } | |
| if !modelsDev.negativeFresh(model) { | |
| // 与 ContextWindowListingV4 同触发:lookup 命中先入缓存再返回 | |
| // (两条查找链并发 miss 同一模型时,第二调用方直接拿到刚写入的值)。 | |
| if e, ok := modelsDev.lookup(model); ok { | |
| modelCatalogPut(model, e.Context, e.Output) | |
| return e.Output, e.Output > 0 | |
| } | |
| modelsDev.ensureDocAsync(client, "") | |
| } | |
| return 0, false // 第 4 级兜底:省略(输出上限不编造) | |
| } | |
| // ---- 第 4 级拉取值的回流(fetchDoc 成功后调用,写 model.json)---- | |
| // noteModelsDevMiss 查询未命中 models.dev 索引 → 负缓存已由 lookup 记录。 | |
| // 本函数是 lookup + 写缓存的粘合层:fetchDoc 拉到文档后对「曾 miss 过的模型」 | |
| // 重查一次并写入 model.json(下次 /v1/models 直接命中第 3 级)。 | |
| func (f *modelsDevFetcher) backfillMisses() { | |
| f.mu.Lock() | |
| doc := f.doc | |
| missed := make([]string, 0, len(f.negatives)) | |
| for m := range f.negatives { | |
| missed = append(missed, m) | |
| } | |
| f.mu.Unlock() | |
| if doc == nil { | |
| return | |
| } | |
| for _, m := range missed { | |
| e, ok := doc[m] | |
| if !ok { | |
| continue // 仍查不到:负缓存 24h 生效,不写 | |
| } | |
| modelCatalogPut(m, e.Context, e.Output) | |
| // 回流成功:清除负缓存条目(该模型已有值,后续走第 3 级缓存, | |
| // 不再进本清单)。 | |
| f.mu.Lock() | |
| delete(f.negatives, m) | |
| f.mu.Unlock() | |
| } | |
| } | |