-
Notifications
You must be signed in to change notification settings - Fork 11.3k
feat: support OpenRouter image generation endpoint #5964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Robinnnnn
wants to merge
4
commits into
QuantumNous:main
Choose a base branch
from
Robinnnnn:openrouter-image-generation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
6915d61
feat: support OpenRouter image generation endpoint
Robinnnnn db1fa91
test: use testify require/assert in OpenRouter image URL tests
Robinnnnn 017cf45
refactor: fold OpenRouter image URL special case into channel switch
Robinnnnn 7684bf2
feat: forward OpenRouter-specific image params captured in Extra
Robinnnnn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package openai | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
|
|
||
| "github.com/QuantumNous/new-api/common" | ||
| "github.com/QuantumNous/new-api/dto" | ||
| ) | ||
|
|
||
| // mergeImageRequestExtra flattens the unknown fields captured in | ||
| // ImageRequest.Extra back into the outbound JSON body. OpenRouter's | ||
| // /v1/images endpoint accepts params outside the OpenAI image schema | ||
| // (aspect_ratio, resolution, seed, input_references, provider), which | ||
| // the generic ImageRequest serialization drops. Known fields always | ||
| // win over Extra entries with the same key. | ||
| func mergeImageRequestExtra(request dto.ImageRequest) (map[string]json.RawMessage, error) { | ||
| base, err := common.Marshal(request) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| var bodyMap map[string]json.RawMessage | ||
| if err := common.Unmarshal(base, &bodyMap); err != nil { | ||
| return nil, err | ||
| } | ||
| for k, v := range request.Extra { | ||
| if _, exists := bodyMap[k]; !exists { | ||
| bodyMap[k] = v | ||
| } | ||
| } | ||
| return bodyMap, nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| package openai | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "testing" | ||
|
|
||
| "github.com/QuantumNous/new-api/common" | ||
| "github.com/QuantumNous/new-api/constant" | ||
| "github.com/QuantumNous/new-api/dto" | ||
| relaycommon "github.com/QuantumNous/new-api/relay/common" | ||
| relayconstant "github.com/QuantumNous/new-api/relay/constant" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| // TestGetRequestURLOpenRouterImageGeneration verifies that image generation | ||
| // requests to an OpenRouter channel are sent to OpenRouter's flat | ||
| // {base}/v1/images endpoint instead of the OpenAI-style /v1/images/generations. | ||
| func TestGetRequestURLOpenRouterImageGeneration(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| adaptor := &Adaptor{} | ||
| info := &relaycommon.RelayInfo{ | ||
| RelayMode: relayconstant.RelayModeImagesGenerations, | ||
| RequestURLPath: "/v1/images/generations", | ||
| ChannelMeta: &relaycommon.ChannelMeta{ | ||
| ChannelType: constant.ChannelTypeOpenRouter, | ||
| ChannelBaseUrl: "https://openrouter.ai/api", | ||
| }, | ||
| } | ||
|
|
||
| got, err := adaptor.GetRequestURL(info) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "https://openrouter.ai/api/v1/images", got) | ||
| } | ||
|
|
||
| // TestGetRequestURLOpenRouterChatUnchanged guards against the image special | ||
| // case leaking into the chat completions path for OpenRouter channels. | ||
| func TestGetRequestURLOpenRouterChatUnchanged(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| adaptor := &Adaptor{} | ||
| info := &relaycommon.RelayInfo{ | ||
| RelayMode: relayconstant.RelayModeChatCompletions, | ||
| RequestURLPath: "/v1/chat/completions", | ||
| ChannelMeta: &relaycommon.ChannelMeta{ | ||
| ChannelType: constant.ChannelTypeOpenRouter, | ||
| ChannelBaseUrl: "https://openrouter.ai/api", | ||
| }, | ||
| } | ||
|
|
||
| got, err := adaptor.GetRequestURL(info) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "https://openrouter.ai/api/v1/chat/completions", got) | ||
| } | ||
|
|
||
| // TestConvertImageRequestOpenRouterMergesExtra verifies that OpenRouter-specific | ||
| // image generation params captured in ImageRequest.Extra (aspect_ratio, seed, | ||
| // provider, ...) are merged back into the outbound body for OpenRouter channels, | ||
| // alongside the known OpenAI fields. | ||
| func TestConvertImageRequestOpenRouterMergesExtra(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| body := `{ | ||
| "model": "google/gemini-2.5-flash-image", | ||
| "prompt": "a cat wearing a hat", | ||
| "aspect_ratio": "16:9", | ||
| "seed": 42, | ||
| "provider": {"options": {"only": ["google-vertex"]}} | ||
| }` | ||
| var request dto.ImageRequest | ||
| require.NoError(t, common.Unmarshal([]byte(body), &request)) | ||
| require.Contains(t, request.Extra, "aspect_ratio") | ||
|
|
||
| adaptor := &Adaptor{} | ||
| info := &relaycommon.RelayInfo{ | ||
| RelayMode: relayconstant.RelayModeImagesGenerations, | ||
| RequestURLPath: "/v1/images/generations", | ||
| ChannelMeta: &relaycommon.ChannelMeta{ | ||
| ChannelType: constant.ChannelTypeOpenRouter, | ||
| ChannelBaseUrl: "https://openrouter.ai/api", | ||
| }, | ||
| } | ||
|
|
||
| converted, err := adaptor.ConvertImageRequest(nil, info, request) | ||
| require.NoError(t, err) | ||
|
|
||
| serialized, err := common.Marshal(converted) | ||
| require.NoError(t, err) | ||
|
|
||
| var got map[string]json.RawMessage | ||
| require.NoError(t, common.Unmarshal(serialized, &got)) | ||
|
|
||
| assert.JSONEq(t, `"google/gemini-2.5-flash-image"`, string(got["model"])) | ||
| assert.JSONEq(t, `"a cat wearing a hat"`, string(got["prompt"])) | ||
| assert.JSONEq(t, `"16:9"`, string(got["aspect_ratio"])) | ||
| assert.JSONEq(t, `42`, string(got["seed"])) | ||
| assert.JSONEq(t, `{"options": {"only": ["google-vertex"]}}`, string(got["provider"])) | ||
| } | ||
|
|
||
| // TestConvertImageRequestNonOpenRouterDropsExtra guards the owner's constraint | ||
| // that Extra must NOT be merged globally: for non-OpenRouter channels the | ||
| // serialized body keeps dropping unknown fields. | ||
| func TestConvertImageRequestNonOpenRouterDropsExtra(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| body := `{ | ||
| "model": "gpt-image-1", | ||
| "prompt": "a cat wearing a hat", | ||
| "aspect_ratio": "16:9", | ||
| "seed": 42 | ||
| }` | ||
| var request dto.ImageRequest | ||
| require.NoError(t, common.Unmarshal([]byte(body), &request)) | ||
| require.Contains(t, request.Extra, "aspect_ratio") | ||
|
|
||
| adaptor := &Adaptor{} | ||
| info := &relaycommon.RelayInfo{ | ||
| RelayMode: relayconstant.RelayModeImagesGenerations, | ||
| RequestURLPath: "/v1/images/generations", | ||
| ChannelMeta: &relaycommon.ChannelMeta{ | ||
| ChannelType: constant.ChannelTypeOpenAI, | ||
| ChannelBaseUrl: "https://api.openai.com", | ||
| }, | ||
| } | ||
|
|
||
| converted, err := adaptor.ConvertImageRequest(nil, info, request) | ||
| require.NoError(t, err) | ||
|
|
||
| serialized, err := common.Marshal(converted) | ||
| require.NoError(t, err) | ||
|
|
||
| var got map[string]json.RawMessage | ||
| require.NoError(t, common.Unmarshal(serialized, &got)) | ||
|
|
||
| assert.NotContains(t, got, "aspect_ratio") | ||
| assert.NotContains(t, got, "seed") | ||
| assert.JSONEq(t, `"gpt-image-1"`, string(got["model"])) | ||
| assert.JSONEq(t, `"a cat wearing a hat"`, string(got["prompt"])) | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.