| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- package service
- import (
- "strings"
- "github.com/tidwall/gjson"
- )
- const (
- openAIResponsesEndpoint = "/v1/responses"
- openAIResponsesCompactEndpoint = "/v1/responses/compact"
- responsesLiteHeader = "X-OpenAI-Internal-Codex-Responses-Lite"
- responsesLiteHeaderKey = "x-openai-internal-codex-responses-lite"
- responsesLiteWSMetadataKey = "ws_request_header_x_openai_internal_codex_responses_lite"
- imageGenerationPermissionMessage = "Image generation is not enabled for this group"
- )
- func isOpenAIResponsesLiteHeader(value string) bool {
- return strings.EqualFold(strings.TrimSpace(value), "true")
- }
- func isOpenAIResponsesLiteWebSocketPayload(body []byte) bool {
- if len(body) == 0 || !gjson.ValidBytes(body) {
- return false
- }
- return isOpenAIResponsesLiteHeader(gjson.GetBytes(body, "client_metadata."+responsesLiteWSMetadataKey).String())
- }
- // ImageGenerationPermissionMessage returns the stable end-user error text for disabled groups.
- func ImageGenerationPermissionMessage() string {
- return imageGenerationPermissionMessage
- }
- // GroupAllowsImageGeneration preserves ungrouped-key behavior and enforces the flag when a group is present.
- func GroupAllowsImageGeneration(group *Group) bool {
- return group == nil || group.AllowImageGeneration
- }
- // IsImageGenerationIntent classifies requests that can produce generated images.
- func IsImageGenerationIntent(endpoint string, requestedModel string, body []byte) bool {
- if IsImageGenerationEndpoint(endpoint) {
- return true
- }
- if isOpenAIImageGenerationModel(requestedModel) {
- return true
- }
- if len(body) == 0 || !gjson.ValidBytes(body) {
- return false
- }
- var modelSeen, toolsSeen, inputSeen, toolChoiceSeen bool
- imageIntent := false
- parseRawJSONView(body).ForEach(func(key, value gjson.Result) bool {
- // GetBytes returns the first duplicate key; retain that behavior while walking the root once.
- switch key.Str {
- case "model":
- if !modelSeen {
- modelSeen = true
- imageIntent = isOpenAIImageGenerationModel(strings.TrimSpace(value.String()))
- }
- case "tools":
- if !toolsSeen {
- toolsSeen = true
- imageIntent = openAIJSONToolsContainImageGeneration(value)
- }
- case "input":
- if !inputSeen {
- inputSeen = true
- imageIntent = openAIJSONInputContainsImageGenTool(value)
- }
- case "tool_choice":
- if !toolChoiceSeen {
- toolChoiceSeen = true
- imageIntent = openAIJSONToolChoiceSelectsImageGeneration(value)
- }
- }
- return !imageIntent && (!modelSeen || !toolsSeen || !inputSeen || !toolChoiceSeen)
- })
- return imageIntent
- }
- // IsExplicitImageGenerationIntent 仅检测原生 image_generation 工具、图片模型和显式 tool_choice,
- // 不检测被动的 image_gen namespace 声明。用于 capability 路由决策——被动 namespace 不应
- // 强制要求原生 Responses 能力,否则 Chat Completions-only 账号会被误过滤(#4476)。
- func IsExplicitImageGenerationIntent(endpoint string, requestedModel string, body []byte) bool {
- if IsImageGenerationEndpoint(endpoint) || isOpenAIImageGenerationModel(requestedModel) {
- return true
- }
- if len(body) == 0 || !gjson.ValidBytes(body) {
- return false
- }
- var modelSeen, toolsSeen, toolChoiceSeen bool
- imageIntent := false
- parseRawJSONView(body).ForEach(func(key, value gjson.Result) bool {
- switch key.Str {
- case "model":
- if !modelSeen {
- modelSeen = true
- imageIntent = isOpenAIImageGenerationModel(strings.TrimSpace(value.String()))
- }
- case "tools":
- if !toolsSeen {
- toolsSeen = true
- imageIntent = openAIJSONToolsContainNativeImageGeneration(value)
- }
- case "tool_choice":
- if !toolChoiceSeen {
- toolChoiceSeen = true
- imageIntent = openAIJSONToolChoiceSelectsExplicitImageGeneration(value)
- }
- }
- return !imageIntent && (!modelSeen || !toolsSeen || !toolChoiceSeen)
- })
- return imageIntent
- }
- // IsImageGenerationIntentForPlatform applies platform-specific intent rules.
- //
- // Codex advertises the image_gen namespace on ordinary Responses requests so
- // that it is available if the model needs it. Grok strips namespace and
- // Responses Lite additional_tools declarations before forwarding, so those
- // declarations alone must not turn every Codex request into an image request.
- // Native image_generation tools, explicit image selection and image models
- // remain image intent. Other platforms retain the original declaration rule.
- func IsImageGenerationIntentForPlatform(endpoint string, requestedModel string, body []byte, platform string) bool {
- if !strings.EqualFold(strings.TrimSpace(platform), PlatformGrok) {
- return IsImageGenerationIntent(endpoint, requestedModel, body)
- }
- return isExplicitGrokImageGenerationIntent(endpoint, requestedModel, body)
- }
- func isExplicitGrokImageGenerationIntent(endpoint string, requestedModel string, body []byte) bool {
- if IsImageGenerationEndpoint(endpoint) || isOpenAIImageGenerationModel(requestedModel) {
- return true
- }
- if len(body) == 0 || !gjson.ValidBytes(body) {
- return false
- }
- var modelSeen, toolsSeen, toolChoiceSeen bool
- imageIntent := false
- parseRawJSONView(body).ForEach(func(key, value gjson.Result) bool {
- switch key.Str {
- case "model":
- if !modelSeen {
- modelSeen = true
- imageIntent = isOpenAIImageGenerationModel(strings.TrimSpace(value.String()))
- }
- case "tools":
- if !toolsSeen {
- toolsSeen = true
- // Grok removes namespace catalogs before forwarding. Native
- // image_generation remains an explicit capability request.
- imageIntent = openAIJSONToolsContainNativeImageGeneration(value)
- }
- case "tool_choice":
- if !toolChoiceSeen {
- toolChoiceSeen = true
- imageIntent = openAIJSONToolChoiceSelectsExplicitImageGeneration(value)
- }
- }
- return !imageIntent && (!modelSeen || !toolsSeen || !toolChoiceSeen)
- })
- return imageIntent
- }
- // IsImageGenerationIntentMap is the map-backed variant used after service-side request mutation.
- func IsImageGenerationIntentMap(endpoint string, requestedModel string, reqBody map[string]any) bool {
- if IsImageGenerationEndpoint(endpoint) {
- return true
- }
- if isOpenAIImageGenerationModel(requestedModel) {
- return true
- }
- if reqBody == nil {
- return false
- }
- if isOpenAIImageGenerationModel(firstNonEmptyString(reqBody["model"])) {
- return true
- }
- if hasOpenAIImageGenerationTool(reqBody) {
- return true
- }
- return openAIAnyToolChoiceSelectsImageGeneration(reqBody["tool_choice"])
- }
- // IsImageGenerationEndpoint identifies dedicated generated-image endpoints.
- func IsImageGenerationEndpoint(endpoint string) bool {
- switch normalizeImageGenerationEndpoint(endpoint) {
- case "/v1/images/generations", "/v1/images/edits", "/images/generations", "/images/edits":
- return true
- default:
- return false
- }
- }
- func normalizeImageGenerationEndpoint(endpoint string) string {
- endpoint = strings.TrimSpace(strings.ToLower(endpoint))
- if endpoint == "" {
- return ""
- }
- endpoint = strings.TrimPrefix(endpoint, "https://api.openai.com")
- if idx := strings.IndexByte(endpoint, '?'); idx >= 0 {
- endpoint = endpoint[:idx]
- }
- return strings.TrimRight(endpoint, "/")
- }
- func openAIJSONToolsContainImageGeneration(tools gjson.Result) bool {
- if !tools.IsArray() {
- return false
- }
- found := false
- tools.ForEach(func(_, item gjson.Result) bool {
- if isOpenAIImageGenerationType(openAIJSONString(item.Get("type"))) {
- found = true
- return false
- }
- if isImageGenNamespaceTool(item) {
- found = true
- return false
- }
- return true
- })
- return found
- }
- func openAIJSONToolsContainNativeImageGeneration(tools gjson.Result) bool {
- if !tools.IsArray() {
- return false
- }
- found := false
- tools.ForEach(func(_, item gjson.Result) bool {
- found = isOpenAIImageGenerationType(openAIJSONString(item.Get("type")))
- return !found
- })
- return found
- }
- func isOpenAIImageGenerationType(value string) bool {
- return strings.TrimSpace(value) == "image_generation"
- }
- func isOpenAIImageGenNamespaceName(value string) bool {
- return strings.TrimSpace(value) == "image_gen"
- }
- // isImageGenNamespaceTool detects the namespace advertised by Codex's built-in
- // image-generation extension instead of a hosted image_generation tool.
- func isImageGenNamespaceTool(tool gjson.Result) bool {
- return openAIJSONString(tool.Get("type")) == "namespace" &&
- isOpenAIImageGenNamespaceName(openAIJSONString(tool.Get("name")))
- }
- // openAIJSONInputContainsImageGenTool scans Responses input items for
- // additional_tools entries that declare the image_gen namespace. This covers
- // the "Responses Lite" format where tools are embedded inside input items
- // rather than top-level tools.
- func openAIJSONInputContainsImageGenTool(input gjson.Result) bool {
- if !input.IsArray() {
- return false
- }
- found := false
- input.ForEach(func(_, item gjson.Result) bool {
- if openAIJSONString(item.Get("type")) != "additional_tools" {
- return true
- }
- found = openAIJSONToolsContainImageGeneration(item.Get("tools"))
- return !found
- })
- return found
- }
- func openAIRequestBodyHasImageGenerationDeclaration(body []byte) bool {
- if len(body) == 0 || !gjson.ValidBytes(body) {
- return false
- }
- return openAIJSONToolsContainImageGeneration(gjson.GetBytes(body, "tools")) ||
- openAIJSONInputContainsImageGenTool(gjson.GetBytes(body, "input")) ||
- openAIJSONToolChoiceSelectsImageGeneration(gjson.GetBytes(body, "tool_choice"))
- }
- func openAIRequestBodyImageGenerationToolNeedsNormalization(body []byte) bool {
- if len(body) == 0 || !gjson.ValidBytes(body) {
- return false
- }
- tools := gjson.GetBytes(body, "tools")
- if !tools.IsArray() {
- return false
- }
- needsNormalization := false
- tools.ForEach(func(_, item gjson.Result) bool {
- if openAIJSONString(item.Get("type")) != "image_generation" {
- return true
- }
- // 只有旧字段或明确的模型不兼容字段需要修正时才进入 map 修改。
- if item.Get("format").Exists() || item.Get("compression").Exists() {
- needsNormalization = true
- return false
- }
- imageModel := strings.ToLower(strings.TrimSpace(item.Get("model").String()))
- if strings.HasPrefix(imageModel, "gpt-image-2") && item.Get("input_fidelity").Exists() {
- needsNormalization = true
- return false
- }
- return true
- })
- return needsNormalization
- }
- func openAIJSONToolChoiceSelectsImageGeneration(choice gjson.Result) bool {
- if !choice.Exists() {
- return false
- }
- if choice.Type == gjson.String {
- return isOpenAIImageGenerationType(choice.String())
- }
- if !choice.IsObject() {
- return false
- }
- choiceType := openAIJSONString(choice.Get("type"))
- if isOpenAIImageGenerationType(choiceType) {
- return true
- }
- if choiceType == "namespace" &&
- (isOpenAIImageGenNamespaceName(openAIJSONString(choice.Get("name"))) ||
- isOpenAIImageGenNamespaceName(openAIJSONString(choice.Get("namespace")))) {
- return true
- }
- if tool := choice.Get("tool"); tool.IsObject() && openAIJSONToolChoiceSelectsImageGeneration(tool) {
- return true
- }
- if isOpenAIImageGenerationType(openAIJSONString(choice.Get("function.name"))) {
- return true
- }
- return false
- }
- func openAIJSONToolChoiceSelectsExplicitImageGeneration(choice gjson.Result) bool {
- if openAIJSONToolChoiceSelectsImageGeneration(choice) {
- return true
- }
- if !choice.IsObject() {
- return false
- }
- if tool := choice.Get("tool"); tool.IsObject() && openAIJSONToolChoiceSelectsExplicitImageGeneration(tool) {
- return true
- }
- if isOpenAIImageGenFunctionReference(
- openAIJSONString(choice.Get("namespace")),
- openAIJSONString(choice.Get("name")),
- ) {
- return true
- }
- if fn := choice.Get("function"); fn.IsObject() {
- return isOpenAIImageGenFunctionReference(
- openAIJSONString(fn.Get("namespace")),
- openAIJSONString(fn.Get("name")),
- )
- }
- return false
- }
- func isOpenAIImageGenFunctionReference(namespace string, name string) bool {
- namespace = strings.TrimSpace(namespace)
- name = strings.TrimSpace(name)
- if namespace == "image_gen" && name == "imagegen" {
- return true
- }
- switch name {
- case "image_gen.imagegen", "image_gen__imagegen":
- return true
- default:
- return false
- }
- }
- func openAIAnyToolChoiceSelectsImageGeneration(choice any) bool {
- switch v := choice.(type) {
- case string:
- return isOpenAIImageGenerationType(v)
- case map[string]any:
- choiceType := strings.TrimSpace(firstNonEmptyString(v["type"]))
- if isOpenAIImageGenerationType(choiceType) {
- return true
- }
- if choiceType == "namespace" &&
- (isOpenAIImageGenNamespaceName(firstNonEmptyString(v["name"])) ||
- isOpenAIImageGenNamespaceName(firstNonEmptyString(v["namespace"]))) {
- return true
- }
- if tool, ok := v["tool"].(map[string]any); ok && openAIAnyToolChoiceSelectsImageGeneration(tool) {
- return true
- }
- if fn, ok := v["function"].(map[string]any); ok && isOpenAIImageGenerationType(firstNonEmptyString(fn["name"])) {
- return true
- }
- }
- return false
- }
- func getAPIKeyFromContext(c interface{ Get(string) (any, bool) }) *APIKey {
- if c == nil {
- return nil
- }
- v, exists := c.Get("api_key")
- if !exists {
- return nil
- }
- apiKey, _ := v.(*APIKey)
- return apiKey
- }
- func apiKeyGroup(apiKey *APIKey) *Group {
- if apiKey == nil {
- return nil
- }
- return apiKey.Group
- }
- type OpenAIResponsesImageBillingConfig struct {
- Model string
- SizeTier string
- InputSize string
- }
- func resolveOpenAIResponsesImageBillingConfigDetailed(reqBody map[string]any, fallbackModel string) (OpenAIResponsesImageBillingConfig, error) {
- imageModel := ""
- imageSize := ""
- hasImageTool := false
- if reqBody != nil {
- rawTools, _ := reqBody["tools"].([]any)
- for _, rawTool := range rawTools {
- toolMap, ok := rawTool.(map[string]any)
- if !ok || strings.TrimSpace(firstNonEmptyString(toolMap["type"])) != "image_generation" {
- continue
- }
- hasImageTool = true
- imageModel = strings.TrimSpace(firstNonEmptyString(toolMap["model"]))
- imageSize = strings.TrimSpace(firstNonEmptyString(toolMap["size"]))
- break
- }
- if imageSize == "" {
- imageSize = strings.TrimSpace(firstNonEmptyString(reqBody["size"]))
- }
- }
- if imageModel == "" && reqBody != nil {
- bodyModel := strings.TrimSpace(firstNonEmptyString(reqBody["model"]))
- if isOpenAIImageBillingModelAlias(bodyModel) || !hasImageTool {
- imageModel = bodyModel
- }
- }
- if imageModel == "" && hasImageTool {
- imageModel = "gpt-image-2"
- }
- if imageModel == "" {
- imageModel = strings.TrimSpace(fallbackModel)
- }
- sizeTier := normalizeOpenAIImageSizeTier(imageSize)
- return OpenAIResponsesImageBillingConfig{
- Model: imageModel,
- SizeTier: sizeTier,
- InputSize: imageSize,
- }, nil
- }
- func resolveOpenAIResponsesImageBillingConfigFromBody(body []byte, fallbackModel string) (string, string, error) {
- cfg, err := resolveOpenAIResponsesImageBillingConfigDetailedFromBody(body, fallbackModel)
- if err != nil {
- return "", "", err
- }
- return cfg.Model, cfg.SizeTier, nil
- }
- func resolveOpenAIResponsesImageBillingConfigDetailedFromBody(body []byte, fallbackModel string) (OpenAIResponsesImageBillingConfig, error) {
- imageModel := ""
- imageSize := ""
- hasImageTool := false
- if len(body) > 0 && gjson.ValidBytes(body) {
- tools := gjson.GetBytes(body, "tools")
- if tools.IsArray() {
- tools.ForEach(func(_, item gjson.Result) bool {
- if openAIJSONString(item.Get("type")) != "image_generation" {
- return true
- }
- hasImageTool = true
- imageModel = openAIJSONString(item.Get("model"))
- imageSize = openAIJSONString(item.Get("size"))
- return false
- })
- }
- if imageSize == "" {
- imageSize = openAIJSONString(gjson.GetBytes(body, "size"))
- }
- if imageModel == "" {
- bodyModel := openAIJSONString(gjson.GetBytes(body, "model"))
- if isOpenAIImageBillingModelAlias(bodyModel) || !hasImageTool {
- imageModel = bodyModel
- }
- }
- }
- if imageModel == "" && hasImageTool {
- imageModel = "gpt-image-2"
- }
- if imageModel == "" {
- imageModel = strings.TrimSpace(fallbackModel)
- }
- return OpenAIResponsesImageBillingConfig{
- Model: imageModel,
- SizeTier: normalizeOpenAIImageSizeTier(imageSize),
- InputSize: imageSize,
- }, nil
- }
- func isOpenAIImageBillingModelAlias(model string) bool {
- normalized := strings.ToLower(strings.TrimSpace(model))
- if normalized == "" {
- return false
- }
- return isOpenAIImageGenerationModel(normalized) || strings.Contains(normalized, "image")
- }
- func openAIJSONString(value gjson.Result) string {
- if value.Type != gjson.String {
- return ""
- }
- return strings.TrimSpace(value.String())
- }
|