Skip to content

Commit c4ca8ea

Browse files
mjudeikisclaude
andauthored
fix(app-studio): GPT-5 temperature limits + unconditional sandbox images (#362)
* fix(app-studio): omit temperature for models that fix it (GPT-5, o-series) OpenAI's GPT-5 family and the o1/o3/o4 reasoning models reject any sampling temperature other than the fixed default of 1, returning "this model has beta-limitations, temperature ... are fixed at 1". App Studio hard-coded a custom temperature at three call sites, so any project configured with such a model failed every chat completion. Add projectModelSupportsTemperature/projectTemperatureOptions helpers and gate the temperature on model support in the OpenAI model constructor, project naming, and the turn classifier. Behavior is unchanged for models that accept a custom temperature (e.g. gpt-4o-mini). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-studio): require sandbox images independent of runtimeKubeconfig App Studio creates a SandboxRunner for every project's development environment, and the SandboxRunner spec requires runnerImage and tokenGeneratorImage. The chart only emitted the image env vars (and only validated them) when runtimeKubeconfig.secretName was set, coupling a control-plane concern to the unrelated runtime data plane. With the runtime kubeconfig disabled in prod, the images were never passed and every project create failed with: SandboxRunner ... is invalid: [spec.runnerImage: Required value, spec.tokenGeneratorImage: Required value] Require both images unconditionally so a misconfigured install fails at helm time instead of producing invalid SandboxRunner resources at runtime, and always render the env vars. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b2e2c75 commit c4ca8ea

5 files changed

Lines changed: 68 additions & 26 deletions

File tree

providers/app-studio/api/assistant_eino_model.go

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,57 @@ func newProjectEinoChatModel(ctx context.Context, settings projectLLMSettings) (
5959
}
6060

6161
func newProjectEinoOpenAIChatModel(ctx context.Context, settings projectLLMSettings) (einomodel.BaseChatModel, error) {
62-
temperature := float32(0.2)
63-
model, err := openaimodel.NewChatModel(ctx, &openaimodel.ChatModelConfig{
64-
APIKey: strings.TrimSpace(settings.APIKey),
65-
BaseURL: strings.TrimRight(strings.TrimSpace(settings.BaseURL), "/"),
66-
Model: strings.TrimSpace(settings.Model),
67-
Temperature: &temperature,
68-
HTTPClient: &http.Client{},
69-
})
62+
config := &openaimodel.ChatModelConfig{
63+
APIKey: strings.TrimSpace(settings.APIKey),
64+
BaseURL: strings.TrimRight(strings.TrimSpace(settings.BaseURL), "/"),
65+
Model: strings.TrimSpace(settings.Model),
66+
HTTPClient: &http.Client{},
67+
}
68+
// GPT-5 and the o-series reasoning models reject any temperature other than
69+
// the fixed default of 1, so only request a custom temperature when the
70+
// configured model supports it.
71+
if projectModelSupportsTemperature(settings.Model) {
72+
temperature := float32(0.2)
73+
config.Temperature = &temperature
74+
}
75+
model, err := openaimodel.NewChatModel(ctx, config)
7076
if err != nil {
7177
return nil, fmt.Errorf("create native Eino OpenAI chat model: %w", err)
7278
}
7379
return model, nil
7480
}
7581

82+
// projectModelSupportsTemperature reports whether the given model accepts a
83+
// custom sampling temperature. OpenAI's GPT-5 family and the o-series reasoning
84+
// models (o1/o3/o4) fix temperature, top_p, and the penalties, and return an
85+
// error if a non-default value is sent.
86+
func projectModelSupportsTemperature(model string) bool {
87+
m := strings.ToLower(strings.TrimSpace(model))
88+
if m == "" {
89+
return true
90+
}
91+
// Drop any provider prefix such as "openai/".
92+
if idx := strings.LastIndex(m, "/"); idx >= 0 {
93+
m = m[idx+1:]
94+
}
95+
switch {
96+
case strings.HasPrefix(m, "gpt-5"), strings.HasPrefix(m, "gpt5"):
97+
return false
98+
case strings.HasPrefix(m, "o1"), strings.HasPrefix(m, "o3"), strings.HasPrefix(m, "o4"):
99+
return false
100+
}
101+
return true
102+
}
103+
104+
// projectTemperatureOptions returns the per-call temperature option for the
105+
// given model, or nil when the model does not accept a custom temperature.
106+
func projectTemperatureOptions(model string, temperature float32) []einomodel.Option {
107+
if !projectModelSupportsTemperature(model) {
108+
return nil
109+
}
110+
return []einomodel.Option{einomodel.WithTemperature(temperature)}
111+
}
112+
76113
func newProjectEinoGeminiChatModel(ctx context.Context, settings projectLLMSettings) (einomodel.BaseChatModel, error) {
77114
clientConfig, err := projectEinoGeminiClientConfig(settings)
78115
if err != nil {

providers/app-studio/api/assistant_turn_profile.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,16 @@ func classifyProjectAssistantTurnProfile(history []store.Message) projectAssista
8888
return fallbackProjectAssistantTurnDecision(history).Profile
8989
}
9090

91-
func classifyProjectAssistantTurnWithModel(ctx context.Context, model einomodel.BaseChatModel, history []store.Message) (projectAssistantTurnDecision, error) {
91+
func classifyProjectAssistantTurnWithModel(ctx context.Context, model einomodel.BaseChatModel, history []store.Message, extraOpts ...einomodel.Option) (projectAssistantTurnDecision, error) {
9292
fallback := fallbackProjectAssistantTurnDecision(history)
9393
if model == nil {
9494
return fallback, nil
9595
}
96+
opts := append([]einomodel.Option{einomodel.WithToolChoice(einoschema.ToolChoiceForbidden)}, extraOpts...)
9697
msg, err := model.Generate(ctx, []*einoschema.Message{
9798
einoschema.SystemMessage(projectAssistantTurnClassifierSystemPrompt),
9899
einoschema.UserMessage(projectAssistantTurnClassifierUserPrompt(history)),
99-
}, einomodel.WithTemperature(0), einomodel.WithToolChoice(einoschema.ToolChoiceForbidden))
100+
}, opts...)
100101
if err != nil || msg == nil || len(msg.ToolCalls) > 0 {
101102
return fallback, nil
102103
}
@@ -117,7 +118,7 @@ func projectAssistantSemanticTurnRouter(ctx context.Context, req projectAssistan
117118
if err != nil {
118119
return fallbackProjectAssistantTurnDecision(req.History), nil
119120
}
120-
return classifyProjectAssistantTurnWithModel(ctx, model, req.History)
121+
return classifyProjectAssistantTurnWithModel(ctx, model, req.History, projectTemperatureOptions(req.LLM.Model, 0)...)
121122
}
122123

123124
func projectAssistantFallbackTurnRouter(_ context.Context, req projectAssistantTurnRouteRequest) (projectAssistantTurnDecision, error) {

providers/app-studio/api/llm.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import (
3131
"strings"
3232
"time"
3333

34-
einomodel "github.com/cloudwego/eino/components/model"
3534
einoschema "github.com/cloudwego/eino/schema"
3635
apierrors "k8s.io/apimachinery/pkg/api/errors"
3736
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -415,13 +414,12 @@ func (s *Server) generateProjectNaming(ctx context.Context, c *asclient.Client,
415414
if err != nil {
416415
return projectNamingResult{}, err
417416
}
418-
temperature := float32(0.1)
419417
reply, err := model.Generate(ctx, []*einoschema.Message{
420418
einoschema.SystemMessage("Generate concise app project names. Return only JSON with string fields displayName and repositoryName. " +
421419
"displayName should be 2-5 words, human-readable, and no longer than 64 characters. " +
422420
"repositoryName must be derived from displayName and must already satisfy DNS-1123 label rules: lowercase a-z, 0-9, hyphen only; starts and ends with alphanumeric; max 63 characters."),
423421
einoschema.UserMessage("Prompt:\n" + prompt),
424-
}, einomodel.WithTemperature(temperature))
422+
}, projectTemperatureOptions(settings.Model, 0.1)...)
425423
if err != nil {
426424
return projectNamingResult{}, err
427425
}

providers/app-studio/deploy/chart/templates/deployment.yaml

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@
99
{{- if and $previewGatewayFeatureEnabled (not .Values.previewTokenSecretRef.name) -}}
1010
{{- fail "previewGateway is enabled with baseDomain, but previewTokenSecretRef.name is required for shared preview tokens" -}}
1111
{{- end -}}
12-
{{- if and $runtimeKubeconfigSecret (not $sandboxRunnerImage) -}}
13-
{{- fail "sandboxRunner.runnerImage is required when runtimeKubeconfig.secretName is set; use an immutable digest reference" -}}
12+
{{- /*
13+
App Studio creates a SandboxRunner for every project's development environment,
14+
and the SandboxRunner spec requires both images regardless of whether the
15+
runtime data plane (runtimeKubeconfig) is enabled. Require them unconditionally
16+
so a misconfigured install fails here instead of producing invalid SandboxRunner
17+
resources at runtime.
18+
*/ -}}
19+
{{- if not $sandboxRunnerImage -}}
20+
{{- fail "sandboxRunner.runnerImage is required; use an immutable digest reference" -}}
1421
{{- end -}}
15-
{{- if and $runtimeKubeconfigSecret (not $sandboxTokenGeneratorImage) -}}
16-
{{- fail "sandboxRunner.tokenGeneratorImage is required when runtimeKubeconfig.secretName is set; use an immutable digest reference" -}}
22+
{{- if not $sandboxTokenGeneratorImage -}}
23+
{{- fail "sandboxRunner.tokenGeneratorImage is required; use an immutable digest reference" -}}
1724
{{- end -}}
1825
apiVersion: apps/v1
1926
kind: Deployment
@@ -104,14 +111,12 @@ spec:
104111
- name: APP_STUDIO_RUNTIME_KUBECONFIG
105112
value: /var/run/secrets/kedge/runtime/kubeconfig
106113
{{- end }}
107-
{{- if $sandboxRunnerImage }}
114+
# Always set: the SandboxRunner spec requires both images, and the
115+
# template above fails the install when either is unset.
108116
- name: APP_STUDIO_SANDBOX_RUNNER_IMAGE
109117
value: {{ $sandboxRunnerImage | quote }}
110-
{{- end }}
111-
{{- if $sandboxTokenGeneratorImage }}
112118
- name: APP_STUDIO_SANDBOX_TOKEN_GENERATOR_IMAGE
113119
value: {{ $sandboxTokenGeneratorImage | quote }}
114-
{{- end }}
115120
{{- if $previewGatewayFeatureEnabled }}
116121
- name: APP_STUDIO_PREVIEW_BASE_DOMAIN
117122
value: {{ $previewGatewayBaseDomain | quote }}

providers/app-studio/deploy/chart/values.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ providerKubeconfig:
4141
runtimeKubeconfig:
4242
secretName: ""
4343

44-
# Images passed into infrastructure-backed SandboxRunner resources. They are
45-
# intentionally empty by default so installs do not silently trust mutable
46-
# development tags. Set both to immutable digest references before enabling the
47-
# sandbox runtime kubeconfig.
44+
# Images passed into infrastructure-backed SandboxRunner resources. App Studio
45+
# creates a SandboxRunner for every project, and its spec requires both images,
46+
# so the chart fails the install when either is unset (independent of the
47+
# runtime kubeconfig). They are empty by default so installs do not silently
48+
# trust mutable development tags — set both to immutable digest references.
4849
sandboxRunner:
4950
runnerImage: ""
5051
tokenGeneratorImage: ""

0 commit comments

Comments
 (0)