| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- package service
- import (
- "strings"
- "testing"
- "github.com/stretchr/testify/require"
- )
- func TestIsImageGenerationIntent(t *testing.T) {
- tests := []struct {
- name string
- endpoint string
- model string
- body []byte
- want bool
- }{
- {
- name: "images endpoint",
- endpoint: "/v1/images/generations",
- body: []byte(`{"model":"gpt-image-2"}`),
- want: true,
- },
- {
- name: "image model",
- endpoint: "/v1/responses",
- model: "gpt-image-2",
- body: []byte(`{"model":"gpt-image-2"}`),
- want: true,
- },
- {
- name: "image tool",
- endpoint: "/v1/responses",
- model: "gpt-5.4",
- body: []byte(`{"model":"gpt-5.4","tools":[{"type":"image_generation"}]}`),
- want: true,
- },
- {
- name: "image tool choice",
- endpoint: "/v1/responses",
- model: "gpt-5.4",
- body: []byte(`{"model":"gpt-5.4","tool_choice":{"type":"image_generation"}}`),
- want: true,
- },
- {
- name: "namespace image_gen tool choice",
- endpoint: "/v1/responses",
- model: "gpt-5.5",
- body: []byte(`{"model":"gpt-5.5","tool_choice":{"type":"namespace","name":"image_gen"}}`),
- want: true,
- },
- {
- name: "custom imagegen function tool choice is not image intent",
- endpoint: "/v1/responses",
- model: "gpt-5.5",
- body: []byte(`{"model":"gpt-5.5","tool_choice":{"function":{"name":"imagegen"}}}`),
- want: false,
- },
- {
- name: "required tool choice alone is text",
- endpoint: "/v1/responses",
- model: "gpt-5.4",
- body: []byte(`{"model":"gpt-5.4","tool_choice":"required"}`),
- want: false,
- },
- {
- name: "text only gpt 5.4",
- endpoint: "/v1/responses",
- model: "gpt-5.4",
- body: []byte(`{"model":"gpt-5.4","input":"write code"}`),
- want: false,
- },
- {
- name: "namespace image_gen tool in top-level tools",
- endpoint: "/v1/responses",
- model: "gpt-5.5",
- body: []byte(`{"model":"gpt-5.5","tools":[{"type":"namespace","name":"image_gen","tools":[{"type":"function","name":"imagegen"}]}]}`),
- want: true,
- },
- {
- name: "custom namespace with nested imagegen function is not image intent",
- endpoint: "/v1/responses",
- model: "gpt-5.5",
- body: []byte(`{"model":"gpt-5.5","tools":[{"type":"namespace","name":"media_tools","tools":[{"type":"function","name":"imagegen"}]}]}`),
- want: false,
- },
- {
- name: "namespace image_gen in input additional_tools (Responses Lite)",
- endpoint: "/v1/responses",
- model: "gpt-5.5",
- body: []byte(`{"model":"gpt-5.5","input":[{"type":"additional_tools","role":"developer","tools":[{"type":"namespace","name":"image_gen","tools":[{"type":"function","name":"imagegen"}]}]}]}`),
- want: true,
- },
- {
- name: "non-image namespace tool is not flagged",
- endpoint: "/v1/responses",
- model: "gpt-5.5",
- body: []byte(`{"model":"gpt-5.5","tools":[{"type":"namespace","name":"code_tools","tools":[{"type":"function","name":"run"}]}]}`),
- want: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- require.Equal(t, tt.want, IsImageGenerationIntent(tt.endpoint, tt.model, tt.body))
- })
- }
- }
- func TestIsImageGenerationIntentJSONSemantics(t *testing.T) {
- largeInput := strings.Repeat("x", 1<<20)
- tests := []struct {
- name string
- endpoint string
- body []byte
- want bool
- }{
- {
- name: "chat body image model",
- endpoint: "/v1/chat/completions",
- body: []byte(`{"model":"gpt-image-2"}`),
- want: true,
- },
- {
- name: "large responses input with trailing namespace tool choice",
- endpoint: "/v1/responses",
- body: []byte(`{"model":"gpt-5.5","input":"` + largeInput + `","tool_choice":{"type":"namespace","name":"image_gen"}}`),
- want: true,
- },
- {
- name: "invalid json with image tool",
- endpoint: "/v1/responses",
- body: []byte(`{"tools":[{"type":"image_generation"}]`),
- want: false,
- },
- {
- name: "duplicate model uses first value",
- endpoint: "/v1/responses",
- body: []byte(`{"model":"gpt-5.5","model":"gpt-image-2"}`),
- want: false,
- },
- {
- name: "duplicate null model still uses first value",
- endpoint: "/v1/responses",
- body: []byte(`{"model":null,"model":"gpt-image-2"}`),
- want: false,
- },
- {
- name: "duplicate tools uses first value",
- endpoint: "/v1/responses",
- body: []byte(`{"tools":[],"tools":[{"type":"image_generation"}]}`),
- want: false,
- },
- {
- name: "duplicate input uses first value",
- endpoint: "/v1/responses",
- body: []byte(`{"input":[],"input":[{"type":"additional_tools","tools":[{"type":"namespace","name":"image_gen"}]}]}`),
- want: false,
- },
- {
- name: "duplicate tool choice uses first value",
- endpoint: "/v1/responses",
- body: []byte(`{"tool_choice":"required","tool_choice":{"type":"image_generation"}}`),
- want: false,
- },
- {
- name: "escaped top level key",
- endpoint: "/v1/responses",
- body: []byte(`{"tool_\u0063hoice":{"type":"image_generation"}}`),
- want: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- require.Equal(t, tt.want, IsImageGenerationIntent(tt.endpoint, "gpt-5.5", tt.body))
- })
- }
- }
- func TestIsImageGenerationIntentMap_NamespaceImageGen(t *testing.T) {
- tests := []struct {
- name string
- reqBody map[string]any
- want bool
- }{
- {
- name: "top-level namespace image_gen",
- reqBody: map[string]any{
- "model": "gpt-5.5",
- "tools": []any{
- map[string]any{"type": "namespace", "name": "image_gen", "tools": []any{
- map[string]any{"type": "function", "name": "imagegen"},
- }},
- },
- },
- want: true,
- },
- {
- name: "additional_tools in input",
- reqBody: map[string]any{
- "model": "gpt-5.5",
- "input": []any{
- map[string]any{
- "type": "additional_tools",
- "tools": []any{
- map[string]any{"type": "namespace", "name": "image_gen"},
- },
- },
- },
- },
- want: true,
- },
- {
- name: "custom namespace with nested imagegen function is not image intent",
- reqBody: map[string]any{
- "model": "gpt-5.5",
- "tools": []any{
- map[string]any{
- "type": "namespace",
- "name": "media_tools",
- "tools": []any{
- map[string]any{"type": "function", "name": "imagegen"},
- },
- },
- },
- },
- want: false,
- },
- {
- name: "namespace image_gen tool choice",
- reqBody: map[string]any{
- "model": "gpt-5.5",
- "tool_choice": map[string]any{"type": "namespace", "name": "image_gen"},
- },
- want: true,
- },
- {
- name: "custom imagegen function tool choice is not image intent",
- reqBody: map[string]any{
- "model": "gpt-5.5",
- "tool_choice": map[string]any{
- "function": map[string]any{"name": "imagegen"},
- },
- },
- want: false,
- },
- {
- name: "non-image namespace not flagged",
- reqBody: map[string]any{
- "model": "gpt-5.5",
- "tools": []any{
- map[string]any{"type": "namespace", "name": "code_tools"},
- },
- },
- want: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- require.Equal(t, tt.want, IsImageGenerationIntentMap("/v1/responses", "gpt-5.5", tt.reqBody))
- })
- }
- }
- func TestResolveOpenAIResponsesImageBillingConfigUsesCurrentBodyModel(t *testing.T) {
- imageModel, imageSize, err := resolveOpenAIResponsesImageBillingConfigFromBody(
- []byte(`{"model":"mapped-image-model","tools":[{"type":"image_generation","size":"1024x1024"}]}`),
- "requested-model",
- )
- require.NoError(t, err)
- require.Equal(t, "mapped-image-model", imageModel)
- require.Equal(t, "1K", imageSize)
- }
- func TestResolveOpenAIResponsesImageBillingConfigToolModelWins(t *testing.T) {
- imageModel, imageSize, err := resolveOpenAIResponsesImageBillingConfigFromBody(
- []byte(`{"model":"mapped-text-model","tools":[{"type":"image_generation","model":"gpt-image-2","size":"1536x1024"}]}`),
- "requested-model",
- )
- require.NoError(t, err)
- require.Equal(t, "gpt-image-2", imageModel)
- require.Equal(t, "2K", imageSize)
- }
- func TestResolveOpenAIResponsesImageBillingConfigFromBodyIgnoresUnrelatedLargeInput(t *testing.T) {
- cfg, err := resolveOpenAIResponsesImageBillingConfigDetailedFromBody(
- []byte(`{"model":"mapped-text-model","tools":[{"type":"image_generation","model":"gpt-image-2","size":"2048x1152"}],"input":[{"type":"message","content":[{"type":"input_text","text":"hi","nonce":1e1000000}]}]}`),
- "requested-model",
- )
- require.NoError(t, err)
- require.Equal(t, "gpt-image-2", cfg.Model)
- require.Equal(t, "2K", cfg.SizeTier)
- require.Equal(t, "2048x1152", cfg.InputSize)
- }
- func TestResolveOpenAIResponsesImageBillingConfigSupportsOfficialAndCustomSizes(t *testing.T) {
- tests := []struct {
- name string
- body []byte
- wantTier string
- }{
- {
- name: "official 2k landscape",
- body: []byte(`{"model":"gpt-5.4","tools":[{"type":"image_generation","model":"gpt-image-2","size":"2048x1152"}]}`),
- wantTier: "2K",
- },
- {
- name: "official 4k landscape",
- body: []byte(`{"model":"gpt-5.4","tools":[{"type":"image_generation","model":"gpt-image-2","size":"3840x2160"}]}`),
- wantTier: "4K",
- },
- {
- name: "custom valid 2k",
- body: []byte(`{"model":"gpt-5.5","tools":[{"type":"image_generation","model":"gpt-image-2","size":"1280x768"}]}`),
- wantTier: "2K",
- },
- {
- name: "default image tool model supports flexible size",
- body: []byte(`{"model":"gpt-5.4","tools":[{"type":"image_generation","size":"2048x1152"}]}`),
- wantTier: "2K",
- },
- {
- name: "top level image size is moved into billing",
- body: []byte(`{"model":"gpt-image-2","size":"2048x2048","tools":[{"type":"image_generation","model":"gpt-image-2"}]}`),
- wantTier: "2K",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- imageModel, imageSize, err := resolveOpenAIResponsesImageBillingConfigFromBody(tt.body, "requested-model")
- require.NoError(t, err)
- require.NotEmpty(t, imageModel)
- require.Equal(t, tt.wantTier, imageSize)
- })
- }
- }
- func TestResolveOpenAIResponsesImageBillingConfigDoesNotRejectUnknownSizes(t *testing.T) {
- imageModel, imageSize, err := resolveOpenAIResponsesImageBillingConfigFromBody(
- []byte(`{"model":"gpt-5.4","tools":[{"type":"image_generation","model":"gpt-image-1.5","size":"2048x1152"}]}`),
- "requested-model",
- )
- require.NoError(t, err)
- require.Equal(t, "gpt-image-1.5", imageModel)
- require.Equal(t, "2K", imageSize)
- }
- func TestOpenAIImageOutputCounterDeduplicatesFinalImages(t *testing.T) {
- counter := newOpenAIImageOutputCounter()
- counter.AddSSEData([]byte(`{"type":"response.image_generation_call.partial_image","partial_image_b64":"abc"}`))
- counter.AddSSEData([]byte(`{"type":"response.output_item.done","item":{"id":"ig_1","type":"image_generation_call","result":"final-a","size":"1024x1024"}}`))
- counter.AddSSEData([]byte(`{"type":"response.completed","response":{"output":[{"id":"ig_1","type":"image_generation_call","result":"final-a"},{"id":"ig_2","type":"image_generation_call","result":"final-b","size":"3840x2160"}]}}`))
- require.Equal(t, 2, counter.Count())
- require.Equal(t, []string{"1024x1024", "3840x2160"}, counter.Sizes())
- }
- func TestOpenAIImageOutputCounterCountsImagesAPIStreamShapes(t *testing.T) {
- counter := newOpenAIImageOutputCounter()
- counter.AddSSEData([]byte(`{"type":"image_generation.completed","id":"ig_complete","b64_json":"final-a"}`))
- counter.AddSSEData([]byte(`{"type":"response.output_item.done","item":{"id":"ig_item","type":"image_generation_call","result":"final-b"}}`))
- counter.AddSSEData([]byte(`{"type":"response.completed","response":{"output":[{"id":"ig_done","type":"image_generation_call","result":"final-c"}]}}`))
- require.Equal(t, 3, counter.Count())
- dataCounter := newOpenAIImageOutputCounter()
- dataCounter.AddSSEData([]byte(`{"data":[{"b64_json":"a"},{"b64_json":"b"}]}`))
- dataCounter.AddSSEData([]byte(`{"data":[{"b64_json":"a"},{"b64_json":"b"},{"b64_json":"c"}]}`))
- require.Equal(t, 3, dataCounter.Count())
- }
- func TestOpenAIImageOutputCounterCountsMultilineSSEDataPayload(t *testing.T) {
- counter := newOpenAIImageOutputCounter()
- counter.AddSSEData([]byte("{\"type\":\"image_generation.completed\",\n\"b64_json\":\"final-a\"}"))
- require.Equal(t, 1, counter.Count())
- }
- func TestOpenAIImageOutputCounterCountsMultilineSSEBodyPayload(t *testing.T) {
- counter := newOpenAIImageOutputCounter()
- counter.AddSSEBody(
- "data: {\"type\":\"image_generation.completed\",\n" +
- "data: \"b64_json\":\"final-a\"}\n\n" +
- "data: [DONE]\n\n",
- )
- require.Equal(t, 1, counter.Count())
- }
- func TestOpenAIImageOutputCounterFallsBackForInvalidMultilineSSEBody(t *testing.T) {
- counter := newOpenAIImageOutputCounter()
- counter.AddSSEBody(
- "data: {\"type\":\"image_generation.completed\",\"b64_json\":\"final-a\"}\n" +
- "data: {\"type\":\"image_generation.completed\",\"b64_json\":\"final-b\"}\n\n",
- )
- require.Equal(t, 2, counter.Count())
- }
- func TestCollectOpenAIResponseImageOutputSizesFromJSONBytes(t *testing.T) {
- body := []byte(`{
- "output": [
- {"id":"ig_1","type":"image_generation_call","result":"final-a","size":"3840x2160"},
- {"id":"ig_2","type":"image_generation_call","result":"final-b","size":"1024x1024"}
- ]
- }`)
- require.Equal(t, 2, countOpenAIResponseImageOutputsFromJSONBytes(body))
- require.Equal(t, []string{"3840x2160", "1024x1024"}, collectOpenAIResponseImageOutputSizesFromJSONBytes(body))
- }
- func TestCollectOpenAIResponseImageOutputSizesFromImagesAPIData(t *testing.T) {
- body := []byte(`{
- "data": [
- {"b64_json":"final-a","size":"2048x1152"},
- {"b64_json":"final-b","size":"2048x1152"}
- ]
- }`)
- require.Equal(t, 2, countOpenAIResponseImageOutputsFromJSONBytes(body))
- require.Equal(t, []string{"2048x1152", "2048x1152"}, collectOpenAIResponseImageOutputSizesFromJSONBytes(body))
- }
- func TestCollectOpenAIImageOutputSizesFromSSEBody(t *testing.T) {
- body := "data: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"ig_1\",\"type\":\"image_generation_call\",\"result\":\"final-a\",\"size\":\"3840x2160\"}}\n\n" +
- "data: {\"type\":\"response.completed\",\"response\":{\"output\":[{\"id\":\"ig_1\",\"type\":\"image_generation_call\",\"result\":\"final-a\"},{\"id\":\"ig_2\",\"type\":\"image_generation_call\",\"result\":\"final-b\",\"size\":\"1024x1024\"}]}}\n\n" +
- "data: [DONE]\n\n"
- require.Equal(t, 2, countOpenAIImageOutputsFromSSEBody(body))
- require.Equal(t, []string{"3840x2160", "1024x1024"}, collectOpenAIImageOutputSizesFromSSEBody(body))
- }
|