Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 071a424ca6 Add ANTHROPIC_API_KEY to deployment workflow
Pass the secret to the deploy job and export it for docker-compose.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 07:11:18 -08:00
adminandClaude Opus 4.5 a444b3af88 Add ANTHROPIC_API_KEY to docker-compose environment
Required for Claude LLM provider to work in production.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 07:08:24 -08:00
adminandClaude Opus 4.5 5ea85b5e88 Fix Claude service: add partialCompletion support, update model, fix error handling
- Add partialCompletion support (adds assistant message to continue generation)
- Update default model from claude-3-5-sonnet-20240620 to claude-sonnet-4-20250514
- Replace ??? with proper error logging for unexpected response formats

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 07:03:03 -08:00
adminandClaude Opus 4.5 754a75e323 Treat models without -mini/-nano as reasoning models, add gpt-5.1 option
- Change reasoning model detection: models with -mini or -nano suffix are
  non-reasoning; all others (gpt-5.1, gpt-5.2, o1, o3, etc.) get reasoning_effort
- Add gpt-5.1 to admin console dropdown options

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 21:58:58 -08:00
adminandClaude Opus 4.5 6cf51e723b Only send reasoning_effort for reasoning models (o1, o3)
The reasoning_effort parameter is only valid for OpenAI reasoning models.
Sending it to non-reasoning models like gpt-5-mini causes a 400 Bad Request.

Now only includes reasoning_effort when the model name starts with "o1" or "o3".

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 21:57:41 -08:00
5 changed files with 42 additions and 14 deletions
+2
View File
@@ -161,6 +161,7 @@ jobs:
ADMIN_IMAGE: ${{ needs.build-all.outputs.admin_image_tag }}
JFR_SIDECAR_IMAGE: ${{ needs.build-all.outputs.jfr_sidecar_image_tag }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GPT_MODEL_NAME: ${{ secrets.GPT_MODEL_NAME }}
EAGLE_ENABLE_S3: ${{ secrets.EAGLE_ENABLE_S3 }}
DO_SPACES_ACCESS_KEY: ${{ secrets.DO_SPACES_ACCESS_KEY }}
@@ -279,6 +280,7 @@ jobs:
export ADMIN_IMAGE="${ADMIN_IMAGE}"
export JFR_SIDECAR_IMAGE="${JFR_SIDECAR_IMAGE}"
export OPENAI_API_KEY="${OPENAI_API_KEY}"
export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}"
export GPT_MODEL_NAME="${GPT_MODEL_NAME:-gpt-4o}"
export EAGLE_ENABLE_S3="${EAGLE_ENABLE_S3:-false}"
export DO_SPACES_ACCESS_KEY="${DO_SPACES_ACCESS_KEY}"
+2
View File
@@ -26,6 +26,7 @@ services:
- "40032:40032"
environment:
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
EAGLE_ENABLE_S3: "${EAGLE_ENABLE_S3:-false}"
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
@@ -73,6 +74,7 @@ services:
- "40034:40032" # Different host port for staging
environment:
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
EAGLE_ENABLE_S3: "${EAGLE_ENABLE_S3:-false}"
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
@@ -1169,7 +1169,7 @@ type SettingInfo struct {
// LLM model dropdown options
var llmProviderOptions = []string{"openai", "claude"}
var openAiModelOptions = []string{"gpt-5-mini", "gpt-5.2", "gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini", "o1", "o1-mini", "o3-mini"}
var openAiModelOptions = []string{"gpt-5-mini", "gpt-5.1", "gpt-5.2", "gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini", "o1", "o1-mini", "o3-mini"}
var claudeModelOptions = []string{"claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-3-7-sonnet-20250219", "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"}
type SettingsPageData struct {
@@ -27,12 +27,20 @@ object ClaudeServiceImpl {
"stream" -> true
)
private def messageVector(inputText: String): Vector[Map[String, String]] =
private def messageVector(
inputText: String,
partialCompletion: Option[String]
): Vector[Map[String, String]] =
Vector(
Map(
"role" -> "user",
"content" -> inputText
)
) ++ partialCompletion.map(pc =>
Map(
"role" -> "assistant",
"content" -> pc
)
)
private def baseRequest(timeoutSeconds: Int): HttpRequest.Builder =
@@ -47,15 +55,17 @@ object ClaudeServiceImpl {
class ClaudeServiceImpl(
val timeoutSeconds: Int = 10,
val defaultModelName: String = "claude-3-5-sonnet-20240620"
val defaultModelName: String = "claude-sonnet-4-20250514"
) extends ExternalTextGenerationServiceImpl {
implicit val jsonFormats: DefaultFormats.type = DefaultFormats
private def requestDictionary(
inputText: String
inputText: String,
partialCompletion: Option[String]
): Map[String, Any] =
dictionaryFor(modelName = defaultModelName) + ("messages" -> messageVector(
inputText
inputText,
partialCompletion
))
override def makeRequest(
@@ -71,7 +81,7 @@ class ClaudeServiceImpl(
.POST(
BodyPublishers.ofString(
Serialization.write(
requestDictionary(inputText = inputText)
requestDictionary(inputText = inputText, partialCompletion = partialCompletion)
)
)
)
@@ -99,7 +109,9 @@ class ClaudeServiceImpl(
completed = false
)
)
case _ => ???
case other =>
// May be a different delta type (e.g., tool use), log and skip
println(s"Unexpected content_block_delta format: $other")
}
case JString("content_block_start") => ()
case JString("content_block_stop") => ()
@@ -118,7 +130,9 @@ class ClaudeServiceImpl(
val errorType = (errorDetails \ "type").extract[String]
val errorMessage = (errorDetails \ "message").extract[String]
println(s"Got an \"($errorType)\" error from Claude: $errorMessage")
case _ => ???
case other =>
// Unknown event type, log and skip
println(s"Unknown Claude streaming event type: $other")
}
}
}
@@ -19,14 +19,24 @@ object OpenAIChatCompletionsServiceImpl {
private val baseURL = new URL("https://api.openai.com/v1/chat/completions")
private val temperature: Double = 1.0
// Non-reasoning models have -mini or -nano suffix; all others support reasoning_effort
private val nonReasoningSuffixes = Set("-mini", "-nano")
private def isReasoningModel(modelName: String): Boolean =
!nonReasoningSuffixes.exists(suffix => modelName.contains(suffix))
private def dictionaryFor(
modelName: String
): Map[String, Any] = Map(
"model" -> modelName,
"temperature" -> temperature,
"stream" -> true,
"reasoning_effort" -> "none"
)
): Map[String, Any] = {
val baseParams = Map(
"model" -> modelName,
"temperature" -> temperature,
"stream" -> true
)
// Only include reasoning_effort for reasoning models (o1, o3, etc.)
if isReasoningModel(modelName) then baseParams + ("reasoning_effort" -> "none")
else baseParams
}
private def messageVector(
inputText: String,