Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions internal/core/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ func NewProviderError(provider string, statusCode int, message string, err error
}
}

// NewEmptyProviderResponseError reports that a provider returned no response body (502).
func NewEmptyProviderResponseError(provider string) *GatewayError {
return NewProviderError(provider, http.StatusBadGateway, "provider returned empty response", nil)
}

// NewRateLimitError creates a new rate limit error (429)
func NewRateLimitError(provider string, message string) *GatewayError {
return &GatewayError{
Expand Down
2 changes: 1 addition & 1 deletion internal/gateway/inference_execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ func (o *InferenceOrchestrator) streamResponsesProviderCall(ctx context.Context,
}

func emptyProviderResponseError(providerType string) *core.GatewayError {
return core.NewProviderError(providerType, http.StatusBadGateway, "provider returned empty response", nil)
return core.NewEmptyProviderResponseError(providerType)
}

func emptyProviderStreamError(providerType string) *core.GatewayError {
Expand Down
93 changes: 93 additions & 0 deletions internal/guardrails/clone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package guardrails

import "gomodel/internal/core"

// cloneToolCalls deep-copies tool calls so guardrail rewrites never mutate the
// caller's original message slice.
func cloneToolCalls(toolCalls []core.ToolCall) []core.ToolCall {
if len(toolCalls) == 0 {
return nil
}
cloned := make([]core.ToolCall, len(toolCalls))
for i, toolCall := range toolCalls {
cloned[i] = core.ToolCall{
ID: toolCall.ID,
Type: toolCall.Type,
Function: core.FunctionCall{
Name: toolCall.Function.Name,
Arguments: toolCall.Function.Arguments,
ExtraFields: core.CloneUnknownJSONFields(toolCall.Function.ExtraFields),
},
ExtraFields: core.CloneUnknownJSONFields(toolCall.ExtraFields),
}
}
return cloned
}

func cloneChatMessageEnvelope(message core.Message) core.Message {
return core.Message{
Role: message.Role,
ToolCallID: message.ToolCallID,
ContentNull: message.ContentNull,
Content: cloneMessageContent(message.Content),
ToolCalls: cloneToolCalls(message.ToolCalls),
ExtraFields: core.CloneUnknownJSONFields(message.ExtraFields),
}
}

func cloneMessageContent(content any) any {
switch value := content.(type) {
case nil:
return nil
case string:
return value
case []core.ContentPart:
return cloneContentParts(value)
default:
parts, ok := core.NormalizeContentParts(content)
if !ok {
// Unrecognized content shapes cannot be deep-copied generically, so
// they are returned as-is. Guardrails replace whole content values
// rather than mutating them in place, so sharing the reference is
// safe; chat content is normalized to nil/string/[]ContentPart
// before reaching here, making this branch defensive.
return value
}
return cloneContentParts(parts)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func cloneContentParts(parts []core.ContentPart) []core.ContentPart {
if len(parts) == 0 {
return nil
}
cloned := make([]core.ContentPart, len(parts))
for i, part := range parts {
cloned[i] = cloneContentPart(part)
}
return cloned
}

func cloneContentPart(part core.ContentPart) core.ContentPart {
cloned := core.ContentPart{
Type: part.Type,
Text: part.Text,
ExtraFields: core.CloneUnknownJSONFields(part.ExtraFields),
}
if part.ImageURL != nil {
cloned.ImageURL = &core.ImageURLContent{
URL: part.ImageURL.URL,
Detail: part.ImageURL.Detail,
MediaType: part.ImageURL.MediaType,
ExtraFields: core.CloneUnknownJSONFields(part.ImageURL.ExtraFields),
}
}
if part.InputAudio != nil {
cloned.InputAudio = &core.InputAudioContent{
Data: part.InputAudio.Data,
Format: part.InputAudio.Format,
ExtraFields: core.CloneUnknownJSONFields(part.InputAudio.ExtraFields),
}
}
return cloned
}
52 changes: 0 additions & 52 deletions internal/guardrails/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,6 @@ import (
"gomodel/internal/core"
)

// RequestPatcher applies guardrails to translated requests without owning
// provider execution.
type RequestPatcher struct {
pipeline *Pipeline
}

// NewRequestPatcher creates an explicit translated-request patcher.
func NewRequestPatcher(pipeline *Pipeline) *RequestPatcher {
return &RequestPatcher{pipeline: pipeline}
}

// PatchChatRequest applies guardrails to a translated chat request.
func (p *RequestPatcher) PatchChatRequest(ctx context.Context, req *core.ChatRequest) (*core.ChatRequest, error) {
return processGuardedChat(ctx, p.pipeline, req)
}

// PatchResponsesRequest applies guardrails to a translated responses request.
func (p *RequestPatcher) PatchResponsesRequest(ctx context.Context, req *core.ResponsesRequest) (*core.ResponsesRequest, error) {
return processGuardedResponses(ctx, p.pipeline, req)
}

// BatchPreparer applies guardrails to native batch subrequests before provider
// submission.
type BatchPreparer struct {
provider core.RoutableProvider
pipeline *Pipeline
}

// NewBatchPreparer creates an explicit native-batch preparer.
func NewBatchPreparer(provider core.RoutableProvider, pipeline *Pipeline) *BatchPreparer {
return &BatchPreparer{
provider: provider,
pipeline: pipeline,
}
}

// PrepareBatchRequest applies guardrails to batch subrequests without
// submitting the batch to the wrapped provider.
func (p *BatchPreparer) PrepareBatchRequest(ctx context.Context, providerType string, req *core.BatchRequest) (*core.BatchRewriteResult, error) {
return processGuardedBatchRequest(ctx, providerType, req, p.pipeline, p.batchFileTransport())
}

func (p *BatchPreparer) batchFileTransport() core.BatchFileTransport {
if p == nil || p.provider == nil {
return nil
}
if files, ok := p.provider.(core.NativeFileRoutableProvider); ok {
return files
}
return nil
}

func processGuardedBatchRequest(
ctx context.Context,
providerType string,
Expand Down
Loading