发布 0.2.0:模型池自愈、探测修正、任务异步触发与删除加固
This commit is contained in:
@@ -76,3 +76,34 @@ func ServiceStatus(err error) (int, bool) {
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// IsOnDemandUnsupported 识别「模型在该区域仅为微调基座、不支持按需调用」的 400:
|
||||
// OCI 消息形如 "Not allowed to call finetune base model …, use Endpoint: false"。
|
||||
// ListModels 无字段可事先区分,只能在调用报错时识别并换渠道。
|
||||
func IsOnDemandUnsupported(err error) bool {
|
||||
var svcErr common.ServiceError
|
||||
if !errors.As(err, &svcErr) {
|
||||
return false
|
||||
}
|
||||
return svcErr.GetHTTPStatusCode() == 400 &&
|
||||
strings.Contains(strings.ToLower(svcErr.GetMessage()), "finetune base model")
|
||||
}
|
||||
|
||||
// IsEntityNotFound 识别「实体不存在」404(消息 "Entity with key … not found"):
|
||||
// GenAI 对区域内无按需供给的模型 OCID 返回此类 404,属模型级错误;
|
||||
// 鉴权失败的 404 是 NotAuthorizedOrNotFound(消息为 Authorization failed…),不在此列。
|
||||
func IsEntityNotFound(err error) bool {
|
||||
var svcErr common.ServiceError
|
||||
if !errors.As(err, &svcErr) {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(svcErr.GetMessage())
|
||||
return svcErr.GetHTTPStatusCode() == 404 &&
|
||||
strings.Contains(msg, "entity with key") && strings.Contains(msg, "not found")
|
||||
}
|
||||
|
||||
// IsModelUnavailable 归并「模型×区域不可按需调用」两类报错:微调基座 400 与实体不存在 404;
|
||||
// 命中即应把该 (渠道, 模型) 从池中剔除并换候选/换渠道,而非定论租户配额问题。
|
||||
func IsModelUnavailable(err error) bool {
|
||||
return IsOnDemandUnsupported(err) || IsEntityNotFound(err)
|
||||
}
|
||||
|
||||
@@ -109,3 +109,52 @@ func TestErrorHint(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsOnDemandUnsupported(t *testing.T) {
|
||||
ft := fakeServiceError{status: 400, code: "InvalidParameter",
|
||||
message: "Not allowed to call finetune base model ocid1.generativeaimodel.oc1.eu-frankfurt-1.tpel5q, use Endpoint: false"}
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{"微调基座 400 命中(含包装链)", fmt.Errorf("genai chat: %w", ft), true},
|
||||
{"同消息但非 400 不命中", fakeServiceError{status: 500, code: "InternalError", message: ft.message}, false},
|
||||
{"普通 400 不命中", fakeServiceError{status: 400, code: "InvalidParameter", message: "bad request"}, false},
|
||||
{"非服务端错误不命中", errors.New("finetune base model"), false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsOnDemandUnsupported(tt.err); got != tt.want {
|
||||
t.Errorf("IsOnDemandUnsupported() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsModelUnavailable(t *testing.T) {
|
||||
entity404 := fakeServiceError{status: 404, code: "NotFound",
|
||||
message: "Entity with key ocid1.generativeaimodel.oc1.eu-frankfurt-1.2flsfq not found"}
|
||||
auth404 := fakeServiceError{status: 404, code: "NotAuthorizedOrNotFound",
|
||||
message: "Authorization failed or requested resource not found."}
|
||||
ft400 := fakeServiceError{status: 400, code: "InvalidParameter",
|
||||
message: "Not allowed to call finetune base model ocid1.generativeaimodel.oc1..x, use Endpoint: false"}
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{"实体不存在 404 命中(含包装链)", fmt.Errorf("genai chat: %w", entity404), true},
|
||||
{"鉴权类 404 不命中(仍属租户级)", auth404, false},
|
||||
{"微调基座 400 命中", ft400, true},
|
||||
{"其他 404 消息不命中", fakeServiceError{status: 404, code: "NotFound", message: "route not found"}, false},
|
||||
{"非服务端错误不命中", errors.New("entity with key x not found"), false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsModelUnavailable(tt.err); got != tt.want {
|
||||
t.Errorf("IsModelUnavailable() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+31
-11
@@ -17,11 +17,10 @@ import (
|
||||
|
||||
// GenAiModel 是区域可用基础模型的摘要(管理面 ListModels)。
|
||||
type GenAiModel struct {
|
||||
Ocid string `json:"ocid"`
|
||||
Name string `json:"name"`
|
||||
Vendor string `json:"vendor"`
|
||||
ChatOnly bool `json:"-"`
|
||||
Caps []string `json:"capabilities"`
|
||||
Ocid string `json:"ocid"`
|
||||
Name string `json:"name"`
|
||||
Vendor string `json:"vendor"`
|
||||
Caps []string `json:"capabilities"`
|
||||
// Capability 是网关侧归一能力:CHAT / EMBEDDING(兼具时算 CHAT)
|
||||
Capability string `json:"capability"`
|
||||
// Deprecated 是 OCI 宣布的弃用时间(TimeDeprecated),nil 表示未宣布;
|
||||
@@ -71,18 +70,39 @@ func (c *RealClient) ListGenAiModels(ctx context.Context, cred Credentials, regi
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list genai models: %w", err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
now := time.Now()
|
||||
return dedupGenAiModels(resp.Items, time.Now()), nil
|
||||
}
|
||||
|
||||
// dedupGenAiModels 压平并按名称去重;同名多条目时优先保留不含 FINE_TUNE 能力的条目
|
||||
// (微调基座条目在部分区域不支持按需调用,缓存其 OCID 会导致调用 400)。
|
||||
func dedupGenAiModels(items []generativeai.ModelSummary, now time.Time) []GenAiModel {
|
||||
seen := map[string]int{}
|
||||
var out []GenAiModel
|
||||
for _, m := range resp.Items {
|
||||
for _, m := range items {
|
||||
gm, ok := toGenAiModel(m, now)
|
||||
if !ok || seen[gm.Name] {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
seen[gm.Name] = true
|
||||
if i, dup := seen[gm.Name]; dup {
|
||||
if hasFineTune(out[i].Caps) && !hasFineTune(gm.Caps) {
|
||||
out[i] = gm
|
||||
}
|
||||
continue
|
||||
}
|
||||
seen[gm.Name] = len(out)
|
||||
out = append(out, gm)
|
||||
}
|
||||
return out, nil
|
||||
return out
|
||||
}
|
||||
|
||||
// hasFineTune 判断能力列表是否含 FINE_TUNE(微调基座条目)。
|
||||
func hasFineTune(caps []string) bool {
|
||||
for _, c := range caps {
|
||||
if c == string(generativeai.ModelCapabilityFineTune) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// toGenAiModel 压平模型摘要;无归一能力、无名称或按需推理已退役
|
||||
|
||||
@@ -38,3 +38,33 @@ func TestToGenAiModelRetiredFilter(t *testing.T) {
|
||||
t.Error("正常模型应入池")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDedupGenAiModelsFineTunePreference(t *testing.T) {
|
||||
now := time.Date(2026, 7, 10, 0, 0, 0, 0, time.UTC)
|
||||
chat := []generativeai.ModelCapabilityEnum{generativeai.ModelCapabilityChat}
|
||||
chatFT := []generativeai.ModelCapabilityEnum{generativeai.ModelCapabilityChat, generativeai.ModelCapabilityFineTune}
|
||||
mk := func(id, name string, caps []generativeai.ModelCapabilityEnum) generativeai.ModelSummary {
|
||||
return generativeai.ModelSummary{
|
||||
Id: common.String(id), DisplayName: common.String(name), Vendor: common.String("meta"),
|
||||
Capabilities: caps, LifecycleState: generativeai.ModelLifecycleStateActive,
|
||||
}
|
||||
}
|
||||
const name = "meta.llama-3-70b-instruct"
|
||||
tests := []struct {
|
||||
name string
|
||||
items []generativeai.ModelSummary
|
||||
want string // 期望保留的 OCID
|
||||
}{
|
||||
{"基座在前、纯对话在后:保留纯对话条目", []generativeai.ModelSummary{mk("o-ft", name, chatFT), mk("o-od", name, chat)}, "o-od"},
|
||||
{"纯对话在前、基座在后:保留纯对话条目", []generativeai.ModelSummary{mk("o-od", name, chat), mk("o-ft", name, chatFT)}, "o-od"},
|
||||
{"仅基座条目:保留不误删", []generativeai.ModelSummary{mk("o-ft", name, chatFT)}, "o-ft"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := dedupGenAiModels(tt.items, now)
|
||||
if len(got) != 1 || got[0].Ocid != tt.want {
|
||||
t.Errorf("dedupGenAiModels() = %+v, want 仅保留 %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user