iop/apps/edge/internal/openai/provider_model_rewrite.go

935 lines
29 KiB
Go

package openai
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strings"
"sync"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/config"
)
const (
maxHotPathSelectorProviderInstructionBytes = 4096
maxHotPathSelectorOutputTokens = 4096
hotPathArtifactPairToolName = "iop_write_artifact_pair"
hotPathCallerWorkspacePlanBoundary = "Use the caller workspace current working directory as the task root; never place task outputs under .iop."
)
func buildHotPathSelectorProviderInstruction(requestID string, state selectorInstructionState) (string, error) {
if !validLogicalRequestID(requestID) {
return "", fmt.Errorf("selector artifact request identity is invalid")
}
paths := newReservedPaths(requestID)
var instruction string
switch state {
case selectorInstructionPrepareOnly:
instruction = fmt.Sprintf(`IOP caller-workspace selector instruction.
Operation: prepare-only
Return exactly one admitted prepare tool call for this request-local job directory and no other tool call:
JOB directory: %s
Do not write PLAN or REVIEW in this turn. Do not mention or infer an absolute workspace path.`, paths.JobDir)
case selectorInstructionPairWrite:
instruction = fmt.Sprintf(`IOP caller-workspace selector instruction.
Operation: pair-write
Analyze the immutable user task, then return exactly one iop_write_artifact_pair tool call and no other tool call.
Keep every explicit requirement, exact literal, filename, command, output string, and acceptance condition. Do not add scope.
Write all fields in English using ASCII characters only. Supply one concise goal, 1-5 executable worker steps, and 1-3 observable verification checks.
Do not write markdown; IOP renders the fixed Plan and pending Review templates.
Do not mention or infer an absolute workspace path. Task outputs belong under the caller workspace current working directory, never under .iop unless the user explicitly requests that exact path.
PLAN path: %s
REVIEW path: %s
IOP will append the final Review handoff step and matching pending status itself.`, paths.PlanPath, paths.ReviewPath)
default:
return "", fmt.Errorf("selector provider instruction state is invalid")
}
if len(instruction) > maxHotPathSelectorProviderInstructionBytes {
return "", fmt.Errorf("selector provider instruction exceeds bounded size")
}
return instruction, nil
}
func prepareHotPathSelectorProviderInstruction(tunnel edgeservice.SubmitProviderTunnelRequest, instruction string) (edgeservice.SubmitProviderTunnelRequest, error) {
if strings.TrimSpace(instruction) == "" {
return tunnel, nil
}
rewrite := func(body []byte) ([]byte, error) {
switch config.ProtocolOperation(tunnel.Operation) {
case config.OperationChatCompletions:
return injectHotPathChatSelectorInstruction(body, instruction)
case config.OperationMessages:
return injectHotPathAnthropicSelectorInstruction(body, instruction)
default:
return nil, fmt.Errorf("selector provider instruction does not support operation %q", tunnel.Operation)
}
}
if tunnel.BuildBody != nil {
build := tunnel.BuildBody
tunnel.BuildBody = func(target string) ([]byte, error) {
body, err := build(target)
if err != nil {
return nil, err
}
return rewrite(body)
}
return tunnel, nil
}
if len(tunnel.Body) == 0 {
return tunnel, fmt.Errorf("selector provider body is unavailable")
}
body, err := rewrite(tunnel.Body)
if err != nil {
return tunnel, err
}
tunnel.Body = body
return tunnel, nil
}
func prepareHotPathSelectorOutputLimit(tunnel edgeservice.SubmitProviderTunnelRequest) (edgeservice.SubmitProviderTunnelRequest, error) {
rewrite := func(body []byte) ([]byte, error) {
limitJSON, err := json.Marshal(maxHotPathSelectorOutputTokens)
if err != nil {
return nil, err
}
plan, err := planTopLevelJSONPatches(body, []topLevelJSONPatch{
{name: "max_tokens", value: limitJSON},
{name: "max_completion_tokens"},
})
if err != nil {
return nil, err
}
return plan.apply(), nil
}
if tunnel.BuildBody != nil {
build := tunnel.BuildBody
tunnel.BuildBody = func(target string) ([]byte, error) {
body, err := build(target)
if err != nil {
return nil, err
}
return rewrite(body)
}
return tunnel, nil
}
if len(tunnel.Body) == 0 {
return tunnel, fmt.Errorf("selector provider body is unavailable")
}
body, err := rewrite(tunnel.Body)
if err != nil {
return tunnel, err
}
tunnel.Body = body
return tunnel, nil
}
func prepareHotPathSelectorCanonicalTools(tunnel edgeservice.SubmitProviderTunnelRequest, instruction string, preset config.ExecutionPreset) (edgeservice.SubmitProviderTunnelRequest, error) {
operation := ""
switch {
case strings.Contains(instruction, "Operation: prepare-only"):
operation = "prepare"
case strings.Contains(instruction, "Operation: pair-write"):
operation = "write"
default:
return tunnel, fmt.Errorf("selector canonical tool operation is unavailable")
}
toolNames := make(map[string]struct{})
for _, alternative := range preset.WorkspaceTools {
configured, ok := alternative.Operations[operation]
if ok && strings.TrimSpace(configured.ToolName) != "" {
toolNames[strings.TrimSpace(configured.ToolName)] = struct{}{}
}
}
if len(toolNames) == 0 {
return tunnel, fmt.Errorf("selector canonical tool binding is unavailable")
}
rewrite := func(body []byte) ([]byte, error) {
var root map[string]any
if err := json.Unmarshal(body, &root); err != nil {
return nil, fmt.Errorf("decode selector provider tools: %w", err)
}
actual := make(map[string]map[string]any)
for _, raw := range anySlice(root["tools"]) {
tool, _ := raw.(map[string]any)
function, _ := tool["function"].(map[string]any)
name, _ := function["name"].(string)
if _, ok := toolNames[strings.TrimSpace(name)]; ok {
actual[strings.TrimSpace(name)] = function
}
}
if len(actual) == 0 {
return nil, fmt.Errorf("selector canonical tool is absent from caller tools")
}
if operation == "write" {
root["tools"] = []any{map[string]any{
"type": "function",
"function": map[string]any{
"name": hotPathArtifactPairToolName,
"description": "Return the compact fields used by IOP to render a Plan and pending Review pair.",
"parameters": map[string]any{
"type": "object",
"properties": map[string]any{
"goal": map[string]any{"type": "string", "description": "Concise task goal"},
"steps": map[string]any{
"type": "array", "description": "Executable worker steps", "items": map[string]any{"type": "string"}, "minItems": 1, "maxItems": 5,
},
"verification": map[string]any{
"type": "array", "description": "Observable pass checks", "items": map[string]any{"type": "string"}, "minItems": 1, "maxItems": 3,
},
},
"required": []any{"goal", "steps", "verification"},
"additionalProperties": false,
},
},
}}
root["tool_choice"] = "required"
delete(root, "parallel_tool_calls")
return json.Marshal(root)
}
names := make([]string, 0, len(actual))
for name := range actual {
names = append(names, name)
}
sort.Strings(names)
canonical := make([]any, 0, len(actual))
for _, name := range names {
function := actual[name]
properties := map[string]any{
"path": map[string]any{"type": "string", "description": "IOP-issued relative workspace path"},
}
required := []any{"path"}
if operation == "write" {
properties["content"] = map[string]any{"type": "string", "description": "Complete file content"}
required = append(required, "content")
}
description, _ := function["description"].(string)
canonical = append(canonical, map[string]any{
"type": "function",
"function": map[string]any{
"name": name, "description": strings.TrimSpace(description + " IOP canonical " + operation + " operation."),
"parameters": map[string]any{"type": "object", "properties": properties, "required": required, "additionalProperties": false},
},
})
}
root["tools"] = canonical
root["tool_choice"] = "required"
delete(root, "parallel_tool_calls")
return json.Marshal(root)
}
if tunnel.BuildBody == nil {
return tunnel, fmt.Errorf("selector provider body builder is unavailable")
}
build := tunnel.BuildBody
tunnel.BuildBody = func(target string) ([]byte, error) {
body, err := build(target)
if err != nil {
return nil, err
}
return rewrite(body)
}
return tunnel, nil
}
func injectHotPathChatSelectorInstruction(body []byte, instruction string) ([]byte, error) {
var envelope struct {
Messages []json.RawMessage `json:"messages"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
return nil, fmt.Errorf("decode selector Chat provider body: %w", err)
}
if len(envelope.Messages) == 0 {
return nil, fmt.Errorf("selector Chat provider body has no messages")
}
insertAt := 0
for insertAt < len(envelope.Messages) {
var message struct {
Role string `json:"role"`
}
if err := json.Unmarshal(envelope.Messages[insertAt], &message); err != nil || message.Role != "system" {
break
}
insertAt++
}
injected, err := json.Marshal(map[string]string{"role": "system", "content": instruction})
if err != nil {
return nil, err
}
messages := make([]json.RawMessage, 0, len(envelope.Messages)+1)
messages = append(messages, envelope.Messages[:insertAt]...)
messages = append(messages, injected)
messages = append(messages, envelope.Messages[insertAt:]...)
messagesJSON, err := json.Marshal(messages)
if err != nil {
return nil, err
}
plan, err := planTopLevelJSONPatches(body, []topLevelJSONPatch{{name: "messages", value: messagesJSON}})
if err != nil {
return nil, err
}
return plan.apply(), nil
}
func injectHotPathAnthropicSelectorInstruction(body []byte, instruction string) ([]byte, error) {
fields, _, err := scanTopLevelJSONObject(body)
if err != nil {
return nil, fmt.Errorf("decode selector Messages provider body: %w", err)
}
blocks := make([]json.RawMessage, 0, 2)
for _, field := range fields {
if field.name != "system" {
continue
}
raw := bytes.TrimSpace(body[field.valueFrom:field.valueTo])
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
break
}
if raw[0] == '"' {
var text string
if err := json.Unmarshal(raw, &text); err != nil {
return nil, fmt.Errorf("decode selector Messages system text: %w", err)
}
block, err := json.Marshal(map[string]string{"type": "text", "text": text})
if err != nil {
return nil, err
}
blocks = append(blocks, block)
break
}
if err := json.Unmarshal(raw, &blocks); err != nil {
return nil, fmt.Errorf("decode selector Messages system blocks: %w", err)
}
break
}
injected, err := json.Marshal(map[string]string{"type": "text", "text": instruction})
if err != nil {
return nil, err
}
blocks = append(blocks, injected)
systemJSON, err := json.Marshal(blocks)
if err != nil {
return nil, err
}
plan, err := planTopLevelJSONPatches(body, []topLevelJSONPatch{{name: "system", value: systemJSON}})
if err != nil {
return nil, err
}
return plan.apply(), nil
}
type openAIProviderBodyBuilder struct {
mu sync.Mutex
build func(string) (*openAIRebuiltLease, error)
lease *openAIRebuiltLease
built bool
closed bool
}
func newOpenAIProviderBodyBuilder(build func(string) (*openAIRebuiltLease, error)) *openAIProviderBodyBuilder {
return &openAIProviderBodyBuilder{build: build}
}
func (b *openAIProviderBodyBuilder) BuildBody(target string) ([]byte, error) {
if b == nil {
return nil, fmt.Errorf("OpenAI provider body builder is unavailable")
}
b.mu.Lock()
if b.closed || b.built || b.build == nil {
b.mu.Unlock()
return nil, fmt.Errorf("OpenAI provider body builder is unavailable")
}
b.built = true
build := b.build
b.mu.Unlock()
lease, err := build(target)
if err != nil {
return nil, err
}
b.mu.Lock()
if b.closed {
b.mu.Unlock()
lease.release()
return nil, fmt.Errorf("OpenAI provider body builder is unavailable")
}
b.lease = lease
b.mu.Unlock()
body, err := lease.body()
if err != nil {
b.Close()
return nil, err
}
return body, nil
}
func (b *openAIProviderBodyBuilder) Close() {
if b == nil {
return
}
b.mu.Lock()
b.closed = true
b.build = nil
lease := b.lease
b.lease = nil
b.mu.Unlock()
lease.release()
}
type providerModelRewriter struct {
streaming bool
model string
toolCallWire string
pending []byte
}
func newProviderModelRewriter(streaming bool, model string) *providerModelRewriter {
return newProviderModelRewriterWithToolCallWire(streaming, model, "")
}
func newProviderModelRewriterForDispatch(streaming bool, model string, dispatch edgeservice.RunDispatch) *providerModelRewriter {
return newProviderModelRewriterWithToolCallWire(streaming, model, dispatch.ProfileToolCallWire)
}
func newProviderModelRewriterWithToolCallWire(streaming bool, model, toolCallWire string) *providerModelRewriter {
model = strings.TrimSpace(model)
toolCallWire = strings.TrimSpace(toolCallWire)
if model == "" && toolCallWire == "" {
return nil
}
return &providerModelRewriter{streaming: streaming, model: model, toolCallWire: toolCallWire}
}
func (r *providerModelRewriter) setToolCallWire(toolCallWire string) {
if r != nil {
r.toolCallWire = strings.TrimSpace(toolCallWire)
}
}
func (r *providerModelRewriter) AppendStream(chunk []byte) []byte {
if r == nil || len(chunk) == 0 {
return chunk
}
if !r.streaming {
r.pending = append(r.pending, chunk...)
return nil
}
r.pending = append(r.pending, chunk...)
var out bytes.Buffer
for {
idx := bytes.IndexByte(r.pending, '\n')
if idx < 0 {
break
}
line := r.pending[:idx+1]
out.Write(rewriteProviderSSELine(line, r.model, r.toolCallWire))
r.pending = r.pending[idx+1:]
}
return out.Bytes()
}
func (r *providerModelRewriter) FlushStream() []byte {
if r == nil || len(r.pending) == 0 {
return nil
}
pending := r.pending
r.pending = nil
if !r.streaming {
return r.RewriteComplete(pending)
}
return rewriteProviderSSELine(pending, r.model, r.toolCallWire)
}
func (r *providerModelRewriter) RewriteComplete(body []byte) []byte {
if r == nil || len(body) == 0 {
return body
}
return rewriteProviderJSONResponse(body, r.model, r.toolCallWire)
}
func rewriteProviderSSEModelLine(line []byte, model string) []byte {
return rewriteProviderSSELine(line, model, "")
}
func rewriteProviderSSELine(line []byte, model, toolCallWire string) []byte {
body, ending := splitLineEnding(line)
prefix, payload, ok := bytes.Cut(body, []byte(":"))
if !ok || strings.TrimSpace(string(prefix)) != "data" {
return line
}
payload = bytes.TrimSpace(payload)
if len(payload) == 0 || bytes.Equal(payload, []byte("[DONE]")) {
return line
}
rewritten := rewriteProviderJSONResponse(payload, model, toolCallWire)
if bytes.Equal(rewritten, payload) {
return line
}
out := make([]byte, 0, len("data: ")+len(rewritten)+len(ending))
out = append(out, "data: "...)
out = append(out, rewritten...)
out = append(out, ending...)
return out
}
func rewriteProviderJSONResponse(body []byte, model, toolCallWire string) []byte {
rewritten := rewriteProviderJSONModel(body, model)
if toolCallWire != config.ProtocolToolCallWireGeminiChat {
return rewritten
}
return normalizeGeminiChatProviderResponse(rewritten)
}
func normalizeGeminiChatProviderResponse(body []byte) []byte {
decoder := json.NewDecoder(bytes.NewReader(body))
decoder.UseNumber()
var response map[string]any
if err := decoder.Decode(&response); err != nil {
return body
}
changed := false
for _, rawChoice := range anySlice(response["choices"]) {
choice, ok := rawChoice.(map[string]any)
if !ok {
continue
}
for _, messageKey := range []string{"message", "delta"} {
message, ok := choice[messageKey].(map[string]any)
if !ok {
continue
}
toolCalls := anySlice(message["tool_calls"])
for index, rawCall := range toolCalls {
call, ok := rawCall.(map[string]any)
if !ok {
continue
}
// Gemini's Chat-compatible stream may emit several complete tool
// calls in one delta without OpenAI's per-call index. Preserve their
// positional identity so the stream decoder does not concatenate
// independent argument objects into index zero.
if messageKey == "delta" && len(toolCalls) > 1 {
if _, present := call["index"]; !present {
call["index"] = index
changed = true
}
}
id, idOK := call["id"].(string)
extra, extraOK := call["extra_content"].(map[string]any)
if !idOK || id == "" || !extraOK {
continue
}
google, googleOK := extra["google"].(map[string]any)
if !googleOK {
continue
}
signature, signatureOK := google["thought_signature"].(string)
if !signatureOK || signature == "" {
continue
}
call["id"] = encodeGeminiThoughtSignatureToolID(id, signature)
delete(google, "thought_signature")
if len(google) == 0 {
delete(extra, "google")
}
if len(extra) == 0 {
delete(call, "extra_content")
}
changed = true
}
}
}
if !changed {
return body
}
encoded, err := json.Marshal(response)
if err != nil {
return body
}
return encoded
}
func normalizeGeminiChatProviderRequest(body []byte) ([]byte, error) {
if !bytes.Contains(body, []byte(geminiThoughtSignatureToolIDPrefix)) {
return body, nil
}
decoder := json.NewDecoder(bytes.NewReader(body))
decoder.UseNumber()
var request map[string]any
if err := decoder.Decode(&request); err != nil {
return nil, fmt.Errorf("decode Gemini Chat provider request: %w", err)
}
changed := false
for _, rawMessage := range anySlice(request["messages"]) {
message, ok := rawMessage.(map[string]any)
if !ok {
continue
}
for _, rawCall := range anySlice(message["tool_calls"]) {
call, ok := rawCall.(map[string]any)
if !ok {
continue
}
encodedID, ok := call["id"].(string)
if !ok {
continue
}
id, signature, encoded, err := decodeGeminiThoughtSignatureToolID(encodedID)
if err != nil {
return nil, err
}
if !encoded {
continue
}
if err := restoreGeminiThoughtSignature(call, signature); err != nil {
return nil, err
}
call["id"] = id
changed = true
}
if encodedID, ok := message["tool_call_id"].(string); ok {
id, _, encoded, err := decodeGeminiThoughtSignatureToolID(encodedID)
if err != nil {
return nil, err
}
if encoded {
message["tool_call_id"] = id
changed = true
}
}
}
if !changed {
return body, nil
}
encoded, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("encode Gemini Chat provider request: %w", err)
}
return encoded, nil
}
func restoreGeminiThoughtSignature(call map[string]any, signature string) error {
extra, ok := call["extra_content"].(map[string]any)
if !ok {
if call["extra_content"] != nil {
return fmt.Errorf("restore Gemini thought signature: extra_content is not an object")
}
extra = make(map[string]any)
call["extra_content"] = extra
}
google, ok := extra["google"].(map[string]any)
if !ok {
if extra["google"] != nil {
return fmt.Errorf("restore Gemini thought signature: extra_content.google is not an object")
}
google = make(map[string]any)
extra["google"] = google
}
if existing, exists := google["thought_signature"]; exists && existing != signature {
return fmt.Errorf("restore Gemini thought signature: conflicting signature")
}
google["thought_signature"] = signature
return nil
}
func prepareProviderChatToolCallNormalization(tunnel edgeservice.SubmitProviderTunnelRequest, selected edgeservice.ProviderPoolCandidate) (edgeservice.SubmitProviderTunnelRequest, error) {
if selected.ProtocolProfile == nil {
return tunnel, nil
}
mapping, ok := selected.ProtocolProfile.ToolCallMapping(config.OperationChatCompletions)
if !ok || mapping.Wire != config.ProtocolToolCallWireGeminiChat {
return tunnel, nil
}
build := tunnel.BuildBody
if build == nil {
return tunnel, fmt.Errorf("Gemini Chat provider body builder is unavailable")
}
tunnel.BuildBody = func(target string) ([]byte, error) {
body, err := build(target)
if err != nil {
return nil, err
}
return normalizeGeminiChatProviderRequest(body)
}
return tunnel, nil
}
// prepareProviderChatRequestNormalization applies the bounded Chat field
// aliases owned by the selected protocol profile. A generic OpenAI-compatible
// caller can legally send either token-limit spelling, but current OpenAI Chat
// profiles require max_completion_tokens while Gemini and legacy Chat wires
// use max_tokens. Unknown fields and their original byte ranges remain
// untouched.
func prepareProviderChatRequestNormalization(tunnel edgeservice.SubmitProviderTunnelRequest, selected edgeservice.ProviderPoolCandidate) (edgeservice.SubmitProviderTunnelRequest, error) {
profile := selected.ProtocolProfile
if profile == nil || tunnel.Operation != string(config.OperationChatCompletions) {
return tunnel, nil
}
mapping, ok := profile.EffortMapping(config.OperationChatCompletions)
if !ok {
return tunnel, nil
}
var targetField string
switch mapping.Wire {
case config.ProtocolEffortWireOpenAIChat:
targetField = "max_completion_tokens"
case config.ProtocolEffortWireGeminiChat:
targetField = "max_tokens"
default:
return tunnel, nil
}
rewrite := func(body []byte) ([]byte, error) {
return normalizeChatTokenLimitField(body, targetField)
}
if tunnel.BuildBody != nil {
build := tunnel.BuildBody
tunnel.BuildBody = func(target string) ([]byte, error) {
body, err := build(target)
if err != nil {
return nil, err
}
return rewrite(body)
}
return tunnel, nil
}
if len(tunnel.Body) == 0 {
return tunnel, nil
}
body, err := rewrite(tunnel.Body)
if err != nil {
return tunnel, err
}
tunnel.Body = body
return tunnel, nil
}
// prepareHotPathChatProviderOperation lets an internal caller-workspace stage
// use the closest provider wire without changing the caller-facing Chat
// surface. In particular, OpenAI Chat cannot combine reasoning effort with
// function tools, while the same profile's Responses operation can. The
// internal stage result is already decoded into the common hot-path shape, so
// this operation switch remains private to IOP.
func prepareHotPathChatProviderOperation(
tunnel edgeservice.SubmitProviderTunnelRequest,
selected edgeservice.ProviderPoolCandidate,
requirements providerRequestRequirements,
) (edgeservice.SubmitProviderTunnelRequest, error) {
if selected.ProtocolProfile == nil || tunnel.BuildBody == nil {
return tunnel, errProviderStageMissingBinding
}
prepared, err := singleRequestProviderTunnelPreparer(requirements, tunnel.BuildBody)(tunnel, selected)
if err != nil {
return tunnel, err
}
if prepared.Operation != string(config.OperationChatCompletions) {
return prepared, nil
}
prepared, err = prepareProviderChatRequestNormalization(prepared, selected)
if err != nil {
return tunnel, err
}
return prepareProviderChatToolCallNormalization(prepared, selected)
}
func chatProviderRequirements(req chatCompletionRequest) providerRequestRequirements {
requirements := providerRequestRequirements{
HasTools: len(req.Tools) > 0,
Stream: req.Stream,
StructuredOutput: req.ResponseFormat != nil,
}
if req.ReasoningEffort != nil {
requirements.Effort = strings.TrimSpace(*req.ReasoningEffort)
}
return requirements
}
func normalizeChatTokenLimitField(body []byte, targetField string) ([]byte, error) {
var limits struct {
MaxTokens json.RawMessage `json:"max_tokens"`
MaxCompletionTokens json.RawMessage `json:"max_completion_tokens"`
}
if err := json.Unmarshal(body, &limits); err != nil {
return nil, fmt.Errorf("decode Chat token limit fields: %w", err)
}
var value json.RawMessage
var remove string
switch targetField {
case "max_completion_tokens":
value, remove = limits.MaxCompletionTokens, "max_tokens"
if len(value) == 0 {
value = limits.MaxTokens
}
case "max_tokens":
value, remove = limits.MaxTokens, "max_completion_tokens"
if len(value) == 0 {
value = limits.MaxCompletionTokens
}
default:
return nil, fmt.Errorf("unsupported Chat token limit target %q", targetField)
}
if len(value) == 0 {
return body, nil
}
plan, err := planTopLevelJSONPatches(body, []topLevelJSONPatch{
{name: targetField, value: append(json.RawMessage(nil), value...)},
{name: remove},
})
if err != nil {
return nil, err
}
return plan.apply(), nil
}
func splitLineEnding(line []byte) ([]byte, []byte) {
if len(line) == 0 || line[len(line)-1] != '\n' {
return line, nil
}
if len(line) >= 2 && line[len(line)-2] == '\r' {
return line[:len(line)-2], []byte("\r\n")
}
return line[:len(line)-1], []byte("\n")
}
func rewriteProviderJSONModel(body []byte, model string) []byte {
modelJSON, err := json.Marshal(model)
if err != nil {
return body
}
fields, _, err := scanTopLevelJSONObject(body)
if err != nil {
return body
}
for _, field := range fields {
if field.name == "model" {
plan, err := planTopLevelJSONPatches(body, []topLevelJSONPatch{{name: "model", value: modelJSON}})
if err == nil {
return plan.apply()
}
break
}
}
return body
}
// rewriteChatCompletionModel replaces only the model field of the caller's
// original request JSON so the provider receives its served model name. The
// rest of the caller payload is forwarded without IOP rewriting.
func rewriteChatCompletionModel(rawBody []byte, target string, req chatCompletionRequest) ([]byte, error) {
patches, err := chatCompletionProviderPatches(target, req)
if err != nil {
return nil, err
}
if len(patches) == 0 {
return rawBody, nil
}
plan, err := planTopLevelJSONPatches(rawBody, patches)
if err != nil {
return nil, err
}
return plan.apply(), nil
}
func rewriteChatCompletionModelFromIngress(ingress *openAIIngressSnapshot, target string, req chatCompletionRequest) (*openAIRebuiltLease, error) {
patches, err := chatCompletionProviderPatches(target, req)
if err != nil {
return nil, err
}
return rewriteIngressTopLevelJSON(ingress, patches)
}
func chatCompletionProviderPatches(target string, req chatCompletionRequest) ([]topLevelJSONPatch, error) {
patches := make([]topLevelJSONPatch, 0, 5)
if strings.TrimSpace(target) != "" {
modelJSON, err := json.Marshal(target)
if err != nil {
return nil, err
}
patches = append(patches, topLevelJSONPatch{name: "model", value: modelJSON})
}
if req.MaxTokens != nil {
maxTokensJSON, err := json.Marshal(*req.MaxTokens)
if err != nil {
return nil, err
}
patches = append(patches,
topLevelJSONPatch{name: "max_tokens", value: maxTokensJSON},
topLevelJSONPatch{name: "max_completion_tokens"},
)
}
if req.ThinkingTokenBudget != nil {
budgetJSON, err := json.Marshal(*req.ThinkingTokenBudget)
if err != nil {
return nil, err
}
patches = append(patches, topLevelJSONPatch{name: "thinking_token_budget", value: budgetJSON})
}
if req.Think != nil {
thinkJSON, err := json.Marshal(*req.Think)
if err != nil {
return nil, err
}
patches = append(patches, topLevelJSONPatch{name: "think", value: thinkJSON})
}
return patches, nil
}
// rewriteResponsesModel replaces only the model field of the caller's original
// /v1/responses request JSON so the provider receives its served model name.
// Every other field (input, instructions, tools, max_output_tokens, and any
// Codex/Responses-specific field) is forwarded without IOP rewriting. An empty
// target leaves the body untouched; invalid JSON is rejected.
func rewriteResponsesModel(rawBody []byte, target string) ([]byte, error) {
if strings.TrimSpace(target) == "" {
return rawBody, nil
}
modelJSON, err := json.Marshal(target)
if err != nil {
return nil, err
}
plan, err := planTopLevelJSONPatches(rawBody, []topLevelJSONPatch{{name: "model", value: modelJSON}})
if err != nil {
return nil, err
}
return plan.apply(), nil
}
func rewriteResponsesModelFromIngress(ingress *openAIIngressSnapshot, target string) (*openAIRebuiltLease, error) {
if strings.TrimSpace(target) == "" {
return rewriteIngressTopLevelJSON(ingress, nil)
}
modelJSON, err := json.Marshal(target)
if err != nil {
return nil, err
}
return rewriteIngressTopLevelJSON(ingress, []topLevelJSONPatch{{name: "model", value: modelJSON}})
}
// rewriteIngressTopLevelJSON places both allocation checks around the single
// patched output and returns a lease that stays live through synchronous
// provider submission. The canonical ingress owner remains unchanged.
func rewriteIngressTopLevelJSON(ingress *openAIIngressSnapshot, patches []topLevelJSONPatch) (*openAIRebuiltLease, error) {
body, err := ingress.canonicalBody()
if err != nil {
return nil, err
}
plan, err := planTopLevelJSONPatches(body, patches)
if err != nil {
return nil, err
}
if len(plan.edits) == 0 {
return &openAIRebuiltLease{ingress: ingress, bodyAlias: body}, nil
}
guard, err := ingress.reserveRebuild(int64(plan.outputSize))
if err != nil {
return nil, err
}
output := plan.apply()
rebuilt, err := guard.CommitOwnedTyped(openAIRebuiltBodyViewName, output)
if err != nil {
guard.Close()
return nil, err
}
return &openAIRebuiltLease{
ingress: ingress, guard: guard, rebuilt: rebuilt, bodyAlias: output,
}, nil
}