feat: sync deployed v1.0.0-rc.38-custom.42
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
1 parent
aa6c3f50f4
commit
ceddfd3d6b
828 files changed
+122253
-13702
No files matched your search
@@ -38,6 +38,9 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
|
||||
}
|
||||
|
||||
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
// OpenAI-compatible chat responses are handled by the OpenAI adaptor, which
|
||||
// relies on the thinking-to-content state initialized here.
|
||||
(&openai.Adaptor{}).Init(info)
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
@@ -55,6 +58,9 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
case relayconstant.RelayModeCompletions:
|
||||
return fmt.Sprintf("%s/api/generate", info.ChannelBaseUrl), nil
|
||||
default:
|
||||
if info.ChannelOtherSettings.OllamaOpenAIChat {
|
||||
return fmt.Sprintf("%s/v1/chat/completions", info.ChannelBaseUrl), nil
|
||||
}
|
||||
return fmt.Sprintf("%s/api/chat", info.ChannelBaseUrl), nil
|
||||
}
|
||||
}
|
||||
@@ -83,6 +89,9 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
|
||||
case relayconstant.RelayModeCompletions:
|
||||
return openAIToGenerate(c, request)
|
||||
default:
|
||||
if info.ChannelOtherSettings.OllamaOpenAIChat {
|
||||
return (&openai.Adaptor{}).ConvertOpenAIRequest(c, info, request)
|
||||
}
|
||||
return openAIChatToOllamaChat(c, request)
|
||||
}
|
||||
}
|
||||
@@ -117,6 +126,11 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
adaptor := openai.Adaptor{}
|
||||
return adaptor.DoResponse(c, resp, info)
|
||||
default:
|
||||
// /api/generate always returns native NDJSON; chat follows the channel setting.
|
||||
if info.RelayMode != relayconstant.RelayModeCompletions && info.ChannelOtherSettings.OllamaOpenAIChat {
|
||||
adaptor := openai.Adaptor{}
|
||||
return adaptor.DoResponse(c, resp, info)
|
||||
}
|
||||
if info.IsStream {
|
||||
return ollamaStreamHandler(c, info, resp)
|
||||
}
|
||||
|
||||
@@ -79,6 +79,51 @@ func ollamaToolCallsToOpenAI(toolCalls []OllamaToolCall, startIndex int, include
|
||||
return result, startIndex
|
||||
}
|
||||
|
||||
func buildOllamaStreamDelta(chunk *ollamaChatStreamChunk, responseID string, created int64, model string, toolCallIndex *int) (dto.ChatCompletionsStreamResponse, bool) {
|
||||
delta := dto.ChatCompletionsStreamResponse{
|
||||
Id: responseID,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: created,
|
||||
Model: model,
|
||||
Choices: []dto.ChatCompletionsStreamResponseChoice{{
|
||||
Index: 0,
|
||||
Delta: dto.ChatCompletionsStreamResponseChoiceDelta{Role: "assistant"},
|
||||
}},
|
||||
}
|
||||
|
||||
var content string
|
||||
if chunk.Message != nil {
|
||||
content = chunk.Message.Content
|
||||
} else {
|
||||
content = chunk.Response
|
||||
}
|
||||
if content != "" {
|
||||
delta.Choices[0].Delta.SetContentString(content)
|
||||
}
|
||||
|
||||
hasPayload := content != ""
|
||||
if chunk.Message != nil && len(chunk.Message.Thinking) > 0 {
|
||||
raw := strings.TrimSpace(string(chunk.Message.Thinking))
|
||||
if raw != "" && raw != "null" {
|
||||
// Unmarshal the JSON string to get the actual content without quotes
|
||||
var thinkingContent string
|
||||
if err := common.Unmarshal(chunk.Message.Thinking, &thinkingContent); err == nil {
|
||||
delta.Choices[0].Delta.SetReasoningContent(thinkingContent)
|
||||
} else {
|
||||
// Fallback to raw string if it's not a JSON string
|
||||
delta.Choices[0].Delta.SetReasoningContent(raw)
|
||||
}
|
||||
hasPayload = true
|
||||
}
|
||||
}
|
||||
if chunk.Message != nil && len(chunk.Message.ToolCalls) > 0 {
|
||||
delta.Choices[0].Delta.ToolCalls, *toolCallIndex = ollamaToolCallsToOpenAI(chunk.Message.ToolCalls, *toolCallIndex, true)
|
||||
hasPayload = true
|
||||
}
|
||||
|
||||
return delta, hasPayload
|
||||
}
|
||||
|
||||
func toUnix(ts string) int64 {
|
||||
if ts == "" {
|
||||
return time.Now().Unix()
|
||||
@@ -130,49 +175,18 @@ func ollamaStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
|
||||
created = toUnix(chunk.CreatedAt)
|
||||
|
||||
if !chunk.Done {
|
||||
// delta content
|
||||
var content string
|
||||
if chunk.Message != nil {
|
||||
content = chunk.Message.Content
|
||||
} else {
|
||||
content = chunk.Response
|
||||
}
|
||||
delta := dto.ChatCompletionsStreamResponse{
|
||||
Id: responseId,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: created,
|
||||
Model: model,
|
||||
Choices: []dto.ChatCompletionsStreamResponseChoice{{
|
||||
Index: 0,
|
||||
Delta: dto.ChatCompletionsStreamResponseChoiceDelta{Role: "assistant"},
|
||||
}},
|
||||
}
|
||||
if content != "" {
|
||||
delta.Choices[0].Delta.SetContentString(content)
|
||||
}
|
||||
if chunk.Message != nil && len(chunk.Message.Thinking) > 0 {
|
||||
raw := strings.TrimSpace(string(chunk.Message.Thinking))
|
||||
if raw != "" && raw != "null" {
|
||||
// Unmarshal the JSON string to get the actual content without quotes
|
||||
var thinkingContent string
|
||||
if err := common.Unmarshal(chunk.Message.Thinking, &thinkingContent); err == nil {
|
||||
delta.Choices[0].Delta.SetReasoningContent(thinkingContent)
|
||||
} else {
|
||||
// Fallback to raw string if it's not a JSON string
|
||||
delta.Choices[0].Delta.SetReasoningContent(raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
// tool calls
|
||||
if chunk.Message != nil && len(chunk.Message.ToolCalls) > 0 {
|
||||
delta.Choices[0].Delta.ToolCalls, toolCallIndex = ollamaToolCallsToOpenAI(chunk.Message.ToolCalls, toolCallIndex, true)
|
||||
}
|
||||
delta, _ := buildOllamaStreamDelta(&chunk, responseId, created, model, &toolCallIndex)
|
||||
if data, err := common.Marshal(delta); err == nil {
|
||||
_ = helper.StringData(c, string(data))
|
||||
}
|
||||
continue
|
||||
}
|
||||
// done frame
|
||||
if delta, hasPayload := buildOllamaStreamDelta(&chunk, responseId, created, model, &toolCallIndex); hasPayload {
|
||||
if data, err := common.Marshal(delta); err == nil {
|
||||
_ = helper.StringData(c, string(data))
|
||||
}
|
||||
}
|
||||
// finalize once and break loop
|
||||
usage.PromptTokens = chunk.PromptEvalCount
|
||||
usage.CompletionTokens = chunk.EvalCount
|
||||
|
||||
@@ -97,3 +97,148 @@ func TestOllamaChatHandlerNonStreamToolCalls(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildOllamaStreamDelta(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
raw string
|
||||
startIndex int
|
||||
wantContent string
|
||||
wantReasoning string
|
||||
wantToolCalls []struct {
|
||||
id string
|
||||
name string
|
||||
index int
|
||||
arguments string
|
||||
}
|
||||
wantNextIndex int
|
||||
wantPayload bool
|
||||
}{
|
||||
{
|
||||
name: "chat content",
|
||||
raw: `{"message":{"content":"hello"}}`,
|
||||
wantContent: "hello",
|
||||
wantPayload: true,
|
||||
},
|
||||
{
|
||||
name: "generate content",
|
||||
raw: `{"response":"hello"}`,
|
||||
wantContent: "hello",
|
||||
wantPayload: true,
|
||||
},
|
||||
{
|
||||
name: "thinking json string",
|
||||
raw: `{"message":{"thinking":"consider this"}}`,
|
||||
wantReasoning: "consider this",
|
||||
wantPayload: true,
|
||||
},
|
||||
{
|
||||
name: "thinking raw fallback",
|
||||
raw: `{"message":{"thinking":{"step":"consider this"}}}`,
|
||||
wantReasoning: `{"step":"consider this"}`,
|
||||
wantPayload: true,
|
||||
},
|
||||
{
|
||||
name: "multiple tool calls",
|
||||
raw: `{"message":{"tool_calls":[{"id":"call_upstream","function":{"name":"get_weather","arguments":{"city":"Paris"}}},{"function":{"name":"get_time","arguments":{"timezone":"UTC"}}}]}}`,
|
||||
startIndex: 2,
|
||||
wantToolCalls: []struct {
|
||||
id string
|
||||
name string
|
||||
index int
|
||||
arguments string
|
||||
}{
|
||||
{id: "call_upstream", name: "get_weather", index: 2, arguments: `{"city":"Paris"}`},
|
||||
{id: "call_3", name: "get_time", index: 3, arguments: `{"timezone":"UTC"}`},
|
||||
},
|
||||
wantNextIndex: 4,
|
||||
wantPayload: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var chunk ollamaChatStreamChunk
|
||||
require.NoError(t, common.Unmarshal([]byte(tt.raw), &chunk))
|
||||
|
||||
toolCallIndex := tt.startIndex
|
||||
delta, hasPayload := buildOllamaStreamDelta(&chunk, "response-id", 123, "model", &toolCallIndex)
|
||||
assert.Equal(t, tt.wantPayload, hasPayload)
|
||||
assert.Equal(t, tt.wantContent, delta.Choices[0].Delta.GetContentString())
|
||||
assert.Equal(t, tt.wantReasoning, delta.Choices[0].Delta.GetReasoningContent())
|
||||
assert.Equal(t, tt.wantNextIndex, toolCallIndex)
|
||||
|
||||
if tt.wantToolCalls == nil {
|
||||
assert.Empty(t, delta.Choices[0].Delta.ToolCalls)
|
||||
return
|
||||
}
|
||||
require.Len(t, delta.Choices[0].Delta.ToolCalls, len(tt.wantToolCalls))
|
||||
for i, want := range tt.wantToolCalls {
|
||||
got := delta.Choices[0].Delta.ToolCalls[i]
|
||||
assert.Equal(t, want.id, got.ID)
|
||||
assert.Equal(t, "function", got.Type)
|
||||
assert.Equal(t, want.name, got.Function.Name)
|
||||
assert.Equal(t, want.arguments, got.Function.Arguments)
|
||||
require.NotNil(t, got.Index)
|
||||
assert.Equal(t, want.index, *got.Index)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOllamaStreamHandlerDoneToolCalls(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(`{"model":"qwen3-coder","created_at":"2026-05-27T12:00:00Z","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"get_weather","arguments":{"city":"北京"}}},{"function":{"name":"get_time","arguments":{"timezone":"Asia/Shanghai"}}}]},"done":true,"done_reason":"stop","prompt_eval_count":5,"eval_count":7}`)),
|
||||
}
|
||||
|
||||
usage, apiErr := ollamaStreamHandler(c, &relaycommon.RelayInfo{
|
||||
ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "fallback-model"},
|
||||
}, resp)
|
||||
require.Nil(t, apiErr)
|
||||
require.NotNil(t, usage)
|
||||
assert.Equal(t, 12, usage.TotalTokens)
|
||||
|
||||
var chunks []dto.ChatCompletionsStreamResponse
|
||||
for _, line := range strings.Split(w.Body.String(), "\n") {
|
||||
data, ok := strings.CutPrefix(line, "data: ")
|
||||
if !ok || data == "[DONE]" {
|
||||
continue
|
||||
}
|
||||
var chunk dto.ChatCompletionsStreamResponse
|
||||
require.NoError(t, common.Unmarshal([]byte(data), &chunk))
|
||||
chunks = append(chunks, chunk)
|
||||
}
|
||||
|
||||
var toolChunkIndex, stopChunkIndex int = -1, -1
|
||||
for i := range chunks {
|
||||
if len(chunks[i].Choices) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(chunks[i].Choices[0].Delta.ToolCalls) > 0 {
|
||||
toolChunkIndex = i
|
||||
}
|
||||
if chunks[i].Choices[0].FinishReason != nil {
|
||||
stopChunkIndex = i
|
||||
}
|
||||
}
|
||||
require.NotEqual(t, -1, toolChunkIndex)
|
||||
require.NotEqual(t, -1, stopChunkIndex)
|
||||
assert.Less(t, toolChunkIndex, stopChunkIndex)
|
||||
|
||||
toolCalls := chunks[toolChunkIndex].Choices[0].Delta.ToolCalls
|
||||
require.Len(t, toolCalls, 2)
|
||||
assert.Equal(t, "call_0", toolCalls[0].ID)
|
||||
assert.Equal(t, "call_1", toolCalls[1].ID)
|
||||
require.NotNil(t, toolCalls[0].Index)
|
||||
require.NotNil(t, toolCalls[1].Index)
|
||||
assert.Equal(t, 0, *toolCalls[0].Index)
|
||||
assert.Equal(t, 1, *toolCalls[1].Index)
|
||||
assert.Equal(t, constant.FinishReasonToolCalls, *chunks[stopChunkIndex].Choices[0].FinishReason)
|
||||
assert.Contains(t, w.Body.String(), "data: [DONE]")
|
||||
}
|
||||
Reference in new issue
Block a user