Skip to content

feat: openai/deepseek completion api, close #1458#1554

Merged
looplj merged 1 commit into
unstablefrom
dev-tmp
May 1, 2026
Merged

feat: openai/deepseek completion api, close #1458#1554
looplj merged 1 commit into
unstablefrom
dev-tmp

Conversation

@looplj

@looplj looplj commented May 1, 2026

Copy link
Copy Markdown
Owner

devin-ai-integration[bot]

This comment was marked as resolved.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for the legacy OpenAI Completions API (/v1/completions) across the platform, including frontend response parsing, backend routing, and new transformer implementations for OpenAI and DeepSeek. Key changes include the addition of RequestTypeCompletion and APIFormatOpenAICompletion, along with the necessary logic to aggregate streaming completion chunks. Critical feedback was provided regarding a thread-safety issue in the DeepSeek transformer where request-specific state was stored in a shared field, potentially causing race conditions. Additionally, the reviewer identified the use of a non-standard errors.AsType function that would lead to compilation errors and suggested improvements to JSON unmarshaling logic to ensure errors are correctly propagated.

Comment thread llm/transformer/deepseek/outbound.go Outdated
BaseURL string
APIKeyProvider auth.APIKeyProvider
completion transformer.Outbound
lastRequestType llm.RequestType

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The lastRequestType field in OutboundTransformer introduces a race condition because the transformer instance is shared across multiple concurrent requests within a long-lived service. Storing request-specific state in a shared struct field is not thread-safe.

Comment thread llm/transformer/deepseek/outbound.go Outdated
ctx context.Context,
llmReq *llm.Request,
) (*httpclient.Request, error) {
t.lastRequestType = llmReq.RequestType

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Setting t.lastRequestType here is unsafe as OutboundTransformer is a shared object. Concurrent requests will overwrite this value, leading to incorrect delegation in TransformStream, TransformResponse, and TransformError. Consider passing the request type through the context or using separate transformer instances for different API formats in the channel configuration.

Comment thread llm/transformer/openai/completion_inbound.go
Comment on lines +281 to +287
if llmErr, ok := errors.AsType[*llm.ResponseError](rawErr); ok {
return &httpclient.Error{
StatusCode: llmErr.StatusCode,
Status: http.StatusText(llmErr.StatusCode),
Body: xjson.MustMarshal(&OpenAIError{Detail: llmErr.Detail}),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Similar to the previous issue, errors.AsType is not a standard function. Use errors.As to correctly check for *llm.ResponseError.

Suggested change
if llmErr, ok := errors.AsType[*llm.ResponseError](rawErr); ok {
return &httpclient.Error{
StatusCode: llmErr.StatusCode,
Status: http.StatusText(llmErr.StatusCode),
Body: xjson.MustMarshal(&OpenAIError{Detail: llmErr.Detail}),
}
}
var llmErr *llm.ResponseError
if errors.As(rawErr, &llmErr) {
return &httpclient.Error{
StatusCode: llmErr.StatusCode,
Status: http.StatusText(llmErr.StatusCode),
Body: xjson.MustMarshal(&OpenAIError{Detail: llmErr.Detail}),
}
}

Comment thread llm/completion.go Outdated
return nil
}

return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The UnmarshalJSON method should return an error if it fails to unmarshal the data into both supported formats (string and string array). Returning nil here hides unmarshaling failures, which can lead to unexpected behavior when processing invalid input.

Suggested change
return nil
return err

@greptile-apps

greptile-apps Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds OpenAI-compatible legacy Completion API support (/v1/completions) alongside DeepSeek's /beta/completions endpoint, introduced via new CompletionInboundTransformer and CompletionOutboundTransformer types. It also makes a breaking interface change to transformer.Outbound by threading the original *httpclient.Request through TransformStream and AggregateStreamChunks, enabling composite transformers (DeepSeek) to route by RequestType at stream time — all ~15 implementations are updated consistently.

  • AggregateCompletionStreamChunks concatenates text from all choices into a single index: 0 choice; when n > 1, the aggregated streaming response will contain interleaved, incorrect text for every choice beyond the first.
  • openAICompatibleDefaultEndpoints (backing TypeOpenai and ~10 other providers) does not include APIFormatOpenAICompletion, so the /v1/completions route silently fails for standard OpenAI channels despite the PR title claiming OpenAI support.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 and do not affect the primary single-choice completion path

Previously flagged P1 issues (missing RequestType on outbound request, wrong HTTP status code on error responses) are resolved in the current code. The two remaining findings are edge-case correctness (n > 1 streaming) and a usability gap (OpenAI channel defaults), neither of which blocks the primary use case of single-choice completion via OpenAI or DeepSeek channels.

llm/transformer/openai/completion_aggregator.go (n > 1 aggregation) and internal/server/biz/channel_endpoint.go (openAICompatibleDefaultEndpoints missing completion format)

Important Files Changed

Filename Overview
llm/transformer/openai/completion_outbound.go New outbound transformer for OpenAI completion API; correctly sets RequestType and APIFormat on the returned httpclient.Request, enabling proper routing in composite transformers like DeepSeek
llm/transformer/openai/completion_inbound.go New inbound transformer for OpenAI completion API; correctly uses llmResp.Error.StatusCode for error responses and delegates stream aggregation to AggregateCompletionStreamChunks
llm/transformer/openai/completion_aggregator.go Aggregates streaming completion chunks; has a logic gap when n > 1 — all choices' text is concatenated into a single choice[0] instead of per-index accumulation
llm/transformer/deepseek/outbound.go Adds completion routing to DeepSeek; uses NormalizeBaseURL's '#' sentinel to correctly construct the /beta/completions endpoint URL; routes stream and aggregate calls via req.RequestType
llm/transformer/interfaces.go Breaking interface change: TransformStream and AggregateStreamChunks now take a *httpclient.Request parameter; all 15+ implementations updated consistently
internal/server/biz/channel_endpoint.go Adds APIFormatOpenAICompletion to SupportedAPIFormats and DeepSeek default endpoints; openAICompatibleDefaultEndpoints (used by TypeOpenai and ~10 others) does not include the new format
internal/server/biz/channel_llm.go Adds case for APIFormatOpenAICompletion in buildNonDefaultEndpointOutbound, enabling custom completion endpoints for any channel type
internal/server/orchestrator/custom_endpoints.go Adds completionCapableAPIFormats and routes RequestTypeCompletion in SelectAPIFormatForRequestType
llm/transformer/openai/error.go New shared TransformOpenAIError function extracted for reuse; handles both "error" and "errors" JSON fields from OpenAI-compatible providers
frontend/src/features/requests/utils/response-parser.ts Adds UI support for rendering legacy completion responses (choices[].text) in both non-streaming and streaming paths

Sequence Diagram

sequenceDiagram
    participant Client
    participant Router as /v1/completions route
    participant CIT as CompletionInboundTransformer
    participant Orch as Orchestrator
    participant DS as DeepSeek OutboundTransformer
    participant COT as CompletionOutboundTransformer
    participant API as Provider API

    Client->>Router: POST /v1/completions
    Router->>CIT: TransformRequest(httpReq)
    CIT-->>Orch: llm.Request{RequestType: completion}
    Orch->>DS: TransformRequest(llmReq)
    DS->>DS: route by RequestType == completion
    DS->>COT: TransformRequest(llmReq)
    COT-->>DS: httpclient.Request{URL: /beta/completions, RequestType: completion}
    DS-->>Orch: httpclient.Request
    Orch->>API: POST /beta/completions
    API-->>Orch: HTTP Response / Stream
    Orch->>DS: TransformStream(req, stream) or TransformResponse(resp)
    DS->>DS: route by req.RequestType == completion
    DS->>COT: delegate
    COT-->>DS: llm.Response{Completion: ...}
    DS-->>Orch: llm.Response
    Orch->>CIT: TransformResponse(llmResp)
    CIT-->>Client: CompletionResponse JSON
Loading

Reviews (6): Last reviewed commit: "feat: openai/deepseek completion api, cl..." | Re-trigger Greptile

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment on lines +116 to +127
return &httpclient.Request{
Method: http.MethodPost,
URL: url,
Headers: headers,
Body: body,
Auth: &httpclient.AuthConfig{
Type: "bearer",
APIKey: apiKey,
},
APIFormat: string(llm.APIFormatOpenAICompletion),
Metadata: scope.Metadata(),
}, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CompletionOutboundTransformer.TransformRequest never sets RequestType, breaking DeepSeek stream/response routing

The new CompletionOutboundTransformer.TransformRequest (llm/transformer/openai/completion_outbound.go:116-127) does not set RequestType on the returned httpclient.Request. The DeepSeek outbound transformer relies on req.RequestType to route completion requests: TransformStream at llm/transformer/deepseek/outbound.go:159 checks req.RequestType == string(llm.RequestTypeCompletion), and TransformResponse at llm/transformer/deepseek/outbound.go:167 does the same. Since RequestType is always empty, the condition is never true, and completion streams/responses are incorrectly handled by the base OpenAI chat completions transformer instead of the completion transformer. Other outbound transformers correctly set this field — e.g., image_outbound.go:57 sets rawReq.RequestType = llm.RequestTypeImage.String() and video_outbound.go:90 sets req.RequestType = llm.RequestTypeVideo.String().

Suggested change
return &httpclient.Request{
Method: http.MethodPost,
URL: url,
Headers: headers,
Body: body,
Auth: &httpclient.AuthConfig{
Type: "bearer",
APIKey: apiKey,
},
APIFormat: string(llm.APIFormatOpenAICompletion),
Metadata: scope.Metadata(),
}, nil
return &httpclient.Request{
Method: http.MethodPost,
URL: url,
Headers: headers,
Body: body,
Auth: &httpclient.AuthConfig{
Type: "bearer",
APIKey: apiKey,
},
RequestType: string(llm.RequestTypeCompletion),
APIFormat: string(llm.APIFormatOpenAICompletion),
Metadata: scope.Metadata(),
}, nil
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread llm/completion.go Outdated
greptile-apps[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment on lines +158 to +163
func (t *OutboundTransformer) TransformStream(ctx context.Context, req *httpclient.Request, stream streams.Stream[*httpclient.StreamEvent]) (streams.Stream[*llm.Response], error) {
if req.RequestType == string(llm.RequestTypeCompletion) {
return t.completion.TransformStream(ctx, req, stream)
}

return t.Outbound.TransformStream(ctx, req, stream)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 DeepSeek TransformStream dereferences req without nil check, unlike TransformResponse

In llm/transformer/deepseek/outbound.go:158-163, TransformStream accesses req.RequestType without checking if req is nil. In contrast, the sibling TransformResponse method at llm/transformer/deepseek/outbound.go:167 properly guards with httpResp.Request != nil. The req parameter in the new TransformStream interface signature is *httpclient.Request, which can be nil — several test call sites already pass nil (e.g., pass_through_test.go:758). If TransformStream is ever called with a nil req for a DeepSeek channel, this will cause a nil pointer dereference panic.

Suggested change
func (t *OutboundTransformer) TransformStream(ctx context.Context, req *httpclient.Request, stream streams.Stream[*httpclient.StreamEvent]) (streams.Stream[*llm.Response], error) {
if req.RequestType == string(llm.RequestTypeCompletion) {
return t.completion.TransformStream(ctx, req, stream)
}
return t.Outbound.TransformStream(ctx, req, stream)
func (t *OutboundTransformer) TransformStream(ctx context.Context, req *httpclient.Request, stream streams.Stream[*httpclient.StreamEvent]) (streams.Stream[*llm.Response], error) {
if req != nil && req.RequestType == string(llm.RequestTypeCompletion) {
return t.completion.TransformStream(ctx, req, stream)
}
return t.Outbound.TransformStream(ctx, req, stream)
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@looplj looplj merged commit 17a5fcf into unstable May 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant