Files

1 line
137 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "brsamayu/inner-cosmos", "files": {"scripts/demo/sync-kind-provider-secrets.ps1": {"add4b241338c47dd1ccbafa2880f7b484adb07b0": {"t": 1785595205.9648008, "n": 2880, "h": "19e6363923ba812a", "c": "[CmdletBinding()]\nparam(\n [string]$ExpectedContext = \"kind-kubedeploy\",\n [string]$Namespace = \"inner-cosmos-w3\"\n)\n\n$ErrorActionPreference = \"Stop\"\n$root = (Resolve-Path (Join-Path $PSScriptRoot \"..\\..\")).Path\n$context = (& kubectl config current-context).Trim()\nif ($LASTEXITCODE -ne 0 -or $context -ne $ExpectedContext) {\n throw \"Current context '$context' is not the expected disposable showcase '$ExpectedContext'.\"\n}\n\n$keyFiles = @(Get-ChildItem -LiteralPath $root -File |\n Where-Object { $_.Name.StartsWith(\"API\", [StringComparison]::OrdinalIgnoreCase) -and $_.Extension -eq \".txt\" } |\n Sort-Object Name |\n Select-Object -ExpandProperty FullName)\nif ($keyFiles.Count -eq 0) { throw \"No gitignored API*.txt operator file was found.\" }\n$lines = @($keyFiles | ForEach-Object { Get-Content -LiteralPath $_ -Encoding utf8 })\n\nfunction Match-Key([string]$linePattern, [string]$valuePattern, [string]$label) {\n $line = $lines | Where-Object { $_ -match $linePattern } | Select-Object -First 1\n if (-not $line -or $line -notmatch $valuePattern) { throw \"No valid $label key was found.\" }\n return $Matches[1].Trim().TrimEnd([char]0x3001)\n}\n\n$gemini = Match-Key \"^\\s*gemini\\s*:\" \"^\\s*gemini\\s*:\\s*(\\S+)\" \"Gemini\"\n$deepseek = Match-Key \"(?i)deepseek.*apikey\\s*:\" \"(?i)^.*apikey\\s*:\\s*(\\S+)\" \"DeepSeek\"\n$minimax = Match-Key \"(?i)^\\s*minimax\" \"(sk-[A-Za-z0-9._-]+)\" \"MiniMax\"\n$glm = Match-Key \"(?i)^\\s*glm\" \"([0-9a-fA-F]{32}\\.[A-Za-z0-9_-]+)\" \"GLM\"\n$mimo = Match-Key \"(?i)^\\s*mimo\" \"(tp-[A-Za-z0-9._-]+)\" \"MiMo\"\n$dashscope = Match-Key \"(?i)^\\s*(dashscope|qwen)\\s*:\" \"(?i)^\\s*(?:dashscope|qwen)\\s*:\\s*(\\S+)\" \"DashScope\"\n\n$patchFile = Join-Path ([IO.Path]::GetTempPath()) (\"inner-cosmos-provider-secret-\" + [Guid]::NewGuid() + \".json\")\ntry {\n $patchJson = @{\n stringData = @{\n LLM_API_KEY = $gemini\n GEMINI_API_KEY = $gemini\n DEEPSEEK_API_KEY = $deepseek\n MINIMAX_API_KEY = $minimax\n GLM_API_KEY = $glm\n MIMO_API_KEY = $mimo\n TTS_API_KEY = $dashscope\n }\n } | ConvertTo-Json -Depth 4 -Compress\n [IO.File]::WriteAllText($patchFile, $patchJson, [Text.UTF8Encoding]::new($false))\n & kubectl -n $Namespace patch secret inner-cosmos-runtime --type merge --patch-file $patchFile | Out-Null\n if ($LASTEXITCODE -ne 0) { throw \"Kubernetes provider secret patch failed.\" }\n} finally {\n Remove-Item -LiteralPath $patchFile -Force -ErrorAction SilentlyContinue\n $patchJson = $null\n $gemini = $deepseek = $minimax = $glm = $mimo = $dashscope = $null\n}\n\n& kubectl apply -f (Join-Path $root \"deploy\\k8s\\overlays\\kind-full\\app-config.yaml\") | Out-Null\nif ($LASTEXITCODE -ne 0) { throw \"kind provider ConfigMap apply failed.\" }\nWrite-Output \"KIND_PROVIDER_CONFIGURATION_SYNCED context=$context namespace=$Namespace provider=gemini embedding=false asr=mimo tts=true\"\n"}}, "scripts/demo/run-demo-server.sh": {"add4b241338c47dd1ccbafa2880f7b484adb07b0": {"t": 1785745970.478696, "n": 5010, "h": "893b6cf1bb05dfea", "c": "#!/usr/bin/env bash\n# Demo: start the Inner Cosmos backend on the laptop with a REAL chat provider, reading keys at\n# runtime from the operator's gitignored API及文档.txt. Never writes a\n# key into any committed file. Use this for the in-class demo so the experience runs on real models.\n#\n# Usage: bash scripts/demo/run-demo-server.sh [PORT] (default 8080)\n# Env: DEMO_PORT, JAVA_HOME (defaults to C:\\Program Files\\Java\\jdk-21.0.10 on Windows)\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"$0\")/../..\" && pwd)\"\ncd \"$ROOT\"\nPORT=\"${DEMO_PORT:-${1:-8080}}\"\nKEYS=\"$ROOT/API及文档.txt\"\n\nif [ ! -f \"$KEYS\" ]; then\n echo \"ERROR: $KEYS not found. Put the operator's real keys there (it is gitignored).\" >&2\n exit 1\nfi\n\n# Extract keys without echoing the values (length only, for confirmation).\nQWEN_KEY=\"$(grep -Ei '^(dashscope|qwen):' \"$KEYS\" | head -1 | cut -d: -f2- | sed 's/、$//')\"\nGLM_KEY=\"$(grep -i '^glm' \"$KEYS\" | head -1 | sed -E 's/^[Gg][Ll][Mm][::] *//')\"\nDS_KEY=\"$(grep -i 'deepseek' \"$KEYS\" | head -1 | sed -E 's/.*apikey[::] *//')\"\nif [ -z \"$QWEN_KEY\" ]; then\n echo \"ERROR: could not find the dashscope/qwen line in $KEYS (needed for TTS).\" >&2\n exit 1\nfi\n# Chat provider: DeepSeek by default (proven 0 schema-drift on this HEAD, see\n# evidence/innovation/INNO-EVAL-005). GLM (glm-4-flash) is available but exhibits ~3 first-attempt\n# JSON-parse failures per dual-kernel run, which can surface deterministic fallback text instead of\n# the model's real words -- so it is NOT the default for a demo. Override with DEMO_PROVIDER=glm.\nDEMO_PROVIDER=\"${DEMO_PROVIDER:-deepseek}\"\ncase \"$DEMO_PROVIDER\" in\n deepseek)\n if [ -z \"$DS_KEY\" ]; then echo \"ERROR: DEMO_PROVIDER=deepseek but no deepseek key in $KEYS.\" >&2; exit 1; fi\n CHAT_PROVIDER=\"deepseek\"; CHAT_KEY=\"$DS_KEY\" ;;\n glm)\n if [ -z \"$GLM_KEY\" ]; then echo \"ERROR: DEMO_PROVIDER=glm but no glm: line in $KEYS.\" >&2; exit 1; fi\n CHAT_PROVIDER=\"glm\"; CHAT_KEY=\"$GLM_KEY\"\n echo \"WARNING: GLM has known schema-drift (~3 fallbacks/run); DeepSeek is the recommended demo provider.\" >&2 ;;\n *) echo \"ERROR: DEMO_PROVIDER must be deepseek or glm (got: $DEMO_PROVIDER).\" >&2; exit 1 ;;\nesac\necho \"Loaded key: ${DEMO_PROVIDER}(len=${#CHAT_KEY}) — value not printed.\"\n\n# Keep provider embeddings off; TTS remains enabled.\nexport MEMORY_EMBEDDING_ENABLED=false\nexport TTS_ENABLED=true\nexport TTS_API_KEY=\"$QWEN_KEY\"\nexport TTS_WS_URL=\"wss://llm-errus8cw2pf66bx9.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference\"\n\n# Real chat provider (DeepSeek by default; GLM optional via DEMO_PROVIDER=glm), dev profile (H2).\nexport LLM_MODE=dev\nexport LLM_PROVIDER=\"$CHAT_PROVIDER\"\nexport GLM_API_KEY=\"$GLM_KEY\" # present in env regardless; only used if LLM_PROVIDER=glm\nexport DEEPSEEK_API_KEY=\"$DS_KEY\" # likewise; only used if LLM_PROVIDER=deepseek\nexport LLM_ALLOW_FALLBACK=false # a demo must show the REAL provider, never silently fall back to Mock\n\n# --- Public-tunnel hardening (2026-07-24 8-agent delivery-readiness audit, P1-4/P1-5/P1-7) ---\n# The whole point of this script is to be reachable over a public Cloudflare Tunnel (see\n# docs/demo/DEMO-RUNBOOK.md), so it must not run with plain-localhost-only defaults.\nexport COOKIE_SECURE=true # tunnel is HTTPS end-to-end; never ship an insecure cookie publicly\nexport COOKIE_SAME_SITE=none # the demo APK's WebView origin (https://localhost) is cross-site from\n # the tunnel origin; SameSite=Lax silently drops the session cookie there\nexport INNER_COSMOS_SECURITY_TRUSTED_PROXY_ENABLED=true # honor X-Forwarded-For from cloudflared so rate limits are per-visitor,\n # not one shared bucket for every judge hitting the tunnel from localhost\nexport MANAGEMENT_HEALTH_REDIS_ENABLED=false # this demo path never starts Redis; without this the health\n # endpoint falsely reports DOWN even though the app works fine\n# Classroom experience data is explicit and public-demo-only. It creates three curated,\n# passwordless-entry showcase personas, while the well-known admin seed is disabled. The\n# controller also refuses to operate under the prod profile.\nexport SEED_ENABLED=true\nexport DEMO_PUBLIC_ENTRY_ENABLED=true\nexport DEMO_SEED_ADMIN_ENABLED=false\n\n# JAVA_HOME for the Maven wrapper on Windows.\nif [ -z \"${JAVA_HOME:-}\" ] && [ -d \"/c/Program Files/Java/jdk-21.0.10\" ]; then\n export JAVA_HOME=\"/c/Program Files/Java/jdk-21.0.10\"\nfi\n\necho \"Starting Inner Cosmos on :${PORT} with LLM_PROVIDER=${DEMO_PROVIDER} + embedding disabled + real TTS...\"\necho \"Health check: http://localhost:${PORT}/actuator/health\"\necho \"App: http://localhost:${PORT}/app/aurora/\"\necho \"(Keep this terminal open. Stop with Ctrl+C.)\"\nexec ./mvnw.cmd spring-boot:run -Dspring-boot.run.jvmArguments=\"-Dserver.port=${PORT}\"\n"}, "6305b7ffe384b6039d071fcc05a9b6c36dafd814": {"t": 1785758374.4396384, "n": 5010, "h": "893b6cf1bb05dfea", "c": "#!/usr/bin/env bash\n# Demo: start the Inner Cosmos backend on the laptop with a REAL chat provider, reading keys at\n# runtime from the operator's gitignored API及文档.txt. Never writes a\n# key into any committed file. Use this for the in-class demo so the experience runs on real models.\n#\n# Usage: bash scripts/demo/run-demo-server.sh [PORT] (default 8080)\n# Env: DEMO_PORT, JAVA_HOME (defaults to C:\\Program Files\\Java\\jdk-21.0.10 on Windows)\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"$0\")/../..\" && pwd)\"\ncd \"$ROOT\"\nPORT=\"${DEMO_PORT:-${1:-8080}}\"\nKEYS=\"$ROOT/API及文档.txt\"\n\nif [ ! -f \"$KEYS\" ]; then\n echo \"ERROR: $KEYS not found. Put the operator's real keys there (it is gitignored).\" >&2\n exit 1\nfi\n\n# Extract keys without echoing the values (length only, for confirmation).\nQWEN_KEY=\"$(grep -Ei '^(dashscope|qwen):' \"$KEYS\" | head -1 | cut -d: -f2- | sed 's/、$//')\"\nGLM_KEY=\"$(grep -i '^glm' \"$KEYS\" | head -1 | sed -E 's/^[Gg][Ll][Mm][::] *//')\"\nDS_KEY=\"$(grep -i 'deepseek' \"$KEYS\" | head -1 | sed -E 's/.*apikey[::] *//')\"\nif [ -z \"$QWEN_KEY\" ]; then\n echo \"ERROR: could not find the dashscope/qwen line in $KEYS (needed for TTS).\" >&2\n exit 1\nfi\n# Chat provider: DeepSeek by default (proven 0 schema-drift on this HEAD, see\n# evidence/innovation/INNO-EVAL-005). GLM (glm-4-flash) is available but exhibits ~3 first-attempt\n# JSON-parse failures per dual-kernel run, which can surface deterministic fallback text instead of\n# the model's real words -- so it is NOT the default for a demo. Override with DEMO_PROVIDER=glm.\nDEMO_PROVIDER=\"${DEMO_PROVIDER:-deepseek}\"\ncase \"$DEMO_PROVIDER\" in\n deepseek)\n if [ -z \"$DS_KEY\" ]; then echo \"ERROR: DEMO_PROVIDER=deepseek but no deepseek key in $KEYS.\" >&2; exit 1; fi\n CHAT_PROVIDER=\"deepseek\"; CHAT_KEY=\"$DS_KEY\" ;;\n glm)\n if [ -z \"$GLM_KEY\" ]; then echo \"ERROR: DEMO_PROVIDER=glm but no glm: line in $KEYS.\" >&2; exit 1; fi\n CHAT_PROVIDER=\"glm\"; CHAT_KEY=\"$GLM_KEY\"\n echo \"WARNING: GLM has known schema-drift (~3 fallbacks/run); DeepSeek is the recommended demo provider.\" >&2 ;;\n *) echo \"ERROR: DEMO_PROVIDER must be deepseek or glm (got: $DEMO_PROVIDER).\" >&2; exit 1 ;;\nesac\necho \"Loaded key: ${DEMO_PROVIDER}(len=${#CHAT_KEY}) — value not printed.\"\n\n# Keep provider embeddings off; TTS remains enabled.\nexport MEMORY_EMBEDDING_ENABLED=false\nexport TTS_ENABLED=true\nexport TTS_API_KEY=\"$QWEN_KEY\"\nexport TTS_WS_URL=\"wss://llm-errus8cw2pf66bx9.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference\"\n\n# Real chat provider (DeepSeek by default; GLM optional via DEMO_PROVIDER=glm), dev profile (H2).\nexport LLM_MODE=dev\nexport LLM_PROVIDER=\"$CHAT_PROVIDER\"\nexport GLM_API_KEY=\"$GLM_KEY\" # present in env regardless; only used if LLM_PROVIDER=glm\nexport DEEPSEEK_API_KEY=\"$DS_KEY\" # likewise; only used if LLM_PROVIDER=deepseek\nexport LLM_ALLOW_FALLBACK=false # a demo must show the REAL provider, never silently fall back to Mock\n\n# --- Public-tunnel hardening (2026-07-24 8-agent delivery-readiness audit, P1-4/P1-5/P1-7) ---\n# The whole point of this script is to be reachable over a public Cloudflare Tunnel (see\n# docs/demo/DEMO-RUNBOOK.md), so it must not run with plain-localhost-only defaults.\nexport COOKIE_SECURE=true # tunnel is HTTPS end-to-end; never ship an insecure cookie publicly\nexport COOKIE_SAME_SITE=none # the demo APK's WebView origin (https://localhost) is cross-site from\n # the tunnel origin; SameSite=Lax silently drops the session cookie there\nexport INNER_COSMOS_SECURITY_TRUSTED_PROXY_ENABLED=true # honor X-Forwarded-For from cloudflared so rate limits are per-visitor,\n # not one shared bucket for every judge hitting the tunnel from localhost\nexport MANAGEMENT_HEALTH_REDIS_ENABLED=false # this demo path never starts Redis; without this the health\n # endpoint falsely reports DOWN even though the app works fine\n# Classroom experience data is explicit and public-demo-only. It creates three curated,\n# passwordless-entry showcase personas, while the well-known admin seed is disabled. The\n# controller also refuses to operate under the prod profile.\nexport SEED_ENABLED=true\nexport DEMO_PUBLIC_ENTRY_ENABLED=true\nexport DEMO_SEED_ADMIN_ENABLED=false\n\n# JAVA_HOME for the Maven wrapper on Windows.\nif [ -z \"${JAVA_HOME:-}\" ] && [ -d \"/c/Program Files/Java/jdk-21.0.10\" ]; then\n export JAVA_HOME=\"/c/Program Files/Java/jdk-21.0.10\"\nfi\n\necho \"Starting Inner Cosmos on :${PORT} with LLM_PROVIDER=${DEMO_PROVIDER} + embedding disabled + real TTS...\"\necho \"Health check: http://localhost:${PORT}/actuator/health\"\necho \"App: http://localhost:${PORT}/app/aurora/\"\necho \"(Keep this terminal open. Stop with Ctrl+C.)\"\nexec ./mvnw.cmd spring-boot:run -Dspring-boot.run.jvmArguments=\"-Dserver.port=${PORT}\"\n"}, "946cc55d547dc78388a6eb7f4fa8e73966b4d09e": {"t": 1785758377.2729313, "n": 5282, "h": "b705a00cb3597d45", "c": "#!/usr/bin/env bash\n# Demo: start the Inner Cosmos backend on the laptop with REAL providers (GLM chat + Qwen embedding\n# + Qwen TTS), reading keys at runtime from the operator's gitignored API及文档.txt. Never writes a\n# key into any committed file. Use this for the in-class demo so the experience runs on real models.\n#\n# Usage: bash scripts/demo/run-demo-server.sh [PORT] (default 8080)\n# Env: DEMO_PORT, JAVA_HOME (defaults to C:\\Program Files\\Java\\jdk-21.0.10 on Windows)\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"$0\")/../..\" && pwd)\"\ncd \"$ROOT\"\nPORT=\"${DEMO_PORT:-${1:-8080}}\"\nKEYS=\"$ROOT/API及文档.txt\"\n\nif [ ! -f \"$KEYS\" ]; then\n echo \"ERROR: $KEYS not found. Put the operator's real keys there (it is gitignored).\" >&2\n exit 1\nfi\n\n# Extract keys without echoing the values (length only, for confirmation).\nQWEN_KEY=\"$(grep '^qwen:' \"$KEYS\" | head -1 | cut -d: -f2-)\"\nGLM_KEY=\"$(grep -i '^glm' \"$KEYS\" | head -1 | sed -E 's/^[Gg][Ll][Mm][::] *//')\"\nDS_KEY=\"$(grep -i 'deepseek' \"$KEYS\" | head -1 | sed -E 's/.*apikey[::] *//')\"\nif [ -z \"$QWEN_KEY\" ]; then\n echo \"ERROR: could not find the qwen: line in $KEYS (needed for embedding + TTS).\" >&2\n exit 1\nfi\n# Chat provider: DeepSeek by default (proven 0 schema-drift on this HEAD, see\n# evidence/innovation/INNO-EVAL-005). GLM (glm-4-flash) is available but exhibits ~3 first-attempt\n# JSON-parse failures per dual-kernel run, which can surface deterministic fallback text instead of\n# the model's real words -- so it is NOT the default for a demo. Override with DEMO_PROVIDER=glm.\nDEMO_PROVIDER=\"${DEMO_PROVIDER:-deepseek}\"\ncase \"$DEMO_PROVIDER\" in\n deepseek)\n if [ -z \"$DS_KEY\" ]; then echo \"ERROR: DEMO_PROVIDER=deepseek but no deepseek key in $KEYS.\" >&2; exit 1; fi\n CHAT_PROVIDER=\"deepseek\"; CHAT_KEY=\"$DS_KEY\" ;;\n glm)\n if [ -z \"$GLM_KEY\" ]; then echo \"ERROR: DEMO_PROVIDER=glm but no glm: line in $KEYS.\" >&2; exit 1; fi\n CHAT_PROVIDER=\"glm\"; CHAT_KEY=\"$GLM_KEY\"\n echo \"WARNING: GLM has known schema-drift (~3 fallbacks/run); DeepSeek is the recommended demo provider.\" >&2 ;;\n *) echo \"ERROR: DEMO_PROVIDER must be deepseek or glm (got: $DEMO_PROVIDER).\" >&2; exit 1 ;;\nesac\necho \"Loaded keys: qwen(len=${#QWEN_KEY}) ${DEMO_PROVIDER}(len=${#CHAT_KEY}) — values not printed.\"\n\n# Point embedding + TTS at this account's private Aliyun/DashScope gateway (from API及文档.txt).\nexport MEMORY_EMBEDDING_ENABLED=true\nexport MEMORY_EMBEDDING_API_KEY=\"$QWEN_KEY\"\nexport MEMORY_EMBEDDING_BASE_URL=\"https://llm-errus8cw2pf66bx9.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\nexport MEMORY_EMBEDDING_MODEL=\"text-embedding-v4\"\nexport TTS_ENABLED=true\nexport TTS_API_KEY=\"$QWEN_KEY\"\nexport TTS_WS_URL=\"wss://llm-errus8cw2pf66bx9.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference\"\n\n# Real chat provider (DeepSeek by default; GLM optional via DEMO_PROVIDER=glm), dev profile (H2).\nexport LLM_MODE=dev\nexport LLM_PROVIDER=\"$CHAT_PROVIDER\"\nexport GLM_API_KEY=\"$GLM_KEY\" # present in env regardless; only used if LLM_PROVIDER=glm\nexport DEEPSEEK_API_KEY=\"$DS_KEY\" # likewise; only used if LLM_PROVIDER=deepseek\nexport LLM_ALLOW_FALLBACK=false # a demo must show the REAL provider, never silently fall back to Mock\n\n# --- Public-tunnel hardening (2026-07-24 8-agent delivery-readiness audit, P1-4/P1-5/P1-7) ---\n# The whole point of this script is to be reachable over a public Cloudflare Tunnel (see\n# docs/demo/DEMO-RUNBOOK.md), so it must not run with plain-localhost-only defaults.\nexport COOKIE_SECURE=true # tunnel is HTTPS end-to-end; never ship an insecure cookie publicly\nexport COOKIE_SAME_SITE=none # the demo APK's WebView origin (https://localhost) is cross-site from\n # the tunnel origin; SameSite=Lax silently drops the session cookie there\nexport INNER_COSMOS_SECURITY_TRUSTED_PROXY_ENABLED=true # honor X-Forwarded-For from cloudflared so rate limits are per-visitor,\n # not one shared bucket for every judge hitting the tunnel from localhost\nexport MANAGEMENT_HEALTH_REDIS_ENABLED=false # this demo path never starts Redis; without this the health\n # endpoint falsely reports DOWN even though the app works fine\n# Classroom experience data is explicit and public-demo-only. It creates three curated,\n# passwordless-entry showcase personas, while the well-known admin seed is disabled. The\n# controller also refuses to operate under the prod profile.\nexport SEED_ENABLED=true\nexport DEMO_PUBLIC_ENTRY_ENABLED=true\nexport DEMO_SEED_ADMIN_ENABLED=false\n\n# JAVA_HOME for the Maven wrapper on Windows.\nif [ -z \"${JAVA_HOME:-}\" ] && [ -d \"/c/Program Files/Java/jdk-21.0.10\" ]; then\n export JAVA_HOME=\"/c/Program Files/Java/jdk-21.0.10\"\nfi\n\necho \"Starting Inner Cosmos on :${PORT} with LLM_PROVIDER=${DEMO_PROVIDER} + real embedding + real TTS...\"\necho \"Health check: http://localhost:${PORT}/actuator/health\"\necho \"App: http://localhost:${PORT}/app/aurora/\"\necho \"(Keep this terminal open. Stop with Ctrl+C.)\"\nexec ./mvnw.cmd spring-boot:run -Dspring-boot.run.jvmArguments=\"-Dserver.port=${PORT}\"\n"}, "bfda6e0629bb7342d61ed63b4bbc662fc30d0b4b": {"t": 1785758379.1421645, "n": 5200, "h": "f399ff3cde5d053d", "c": "#!/usr/bin/env bash\n# Demo: start the Inner Cosmos backend on the laptop with REAL providers (GLM chat + Qwen embedding\n# + Qwen TTS), reading keys at runtime from the operator's gitignored API及文档.txt. Never writes a\n# key into any committed file. Use this for the in-class demo so the experience runs on real models.\n#\n# Usage: bash scripts/demo/run-demo-server.sh [PORT] (default 8080)\n# Env: DEMO_PORT, JAVA_HOME (defaults to C:\\Program Files\\Java\\jdk-21.0.10 on Windows)\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"$0\")/../..\" && pwd)\"\ncd \"$ROOT\"\nPORT=\"${DEMO_PORT:-${1:-8080}}\"\nKEYS=\"$ROOT/API及文档.txt\"\n\nif [ ! -f \"$KEYS\" ]; then\n echo \"ERROR: $KEYS not found. Put the operator's real keys there (it is gitignored).\" >&2\n exit 1\nfi\n\n# Extract keys without echoing the values (length only, for confirmation).\nQWEN_KEY=\"$(grep '^qwen:' \"$KEYS\" | head -1 | cut -d: -f2-)\"\nGLM_KEY=\"$(grep -i '^glm' \"$KEYS\" | head -1 | sed -E 's/^[Gg][Ll][Mm][::] *//')\"\nDS_KEY=\"$(grep -i 'deepseek' \"$KEYS\" | head -1 | sed -E 's/.*apikey[::] *//')\"\nif [ -z \"$QWEN_KEY\" ]; then\n echo \"ERROR: could not find the qwen: line in $KEYS (needed for embedding + TTS).\" >&2\n exit 1\nfi\n# Chat provider: DeepSeek by default (proven 0 schema-drift on this HEAD, see\n# evidence/innovation/INNO-EVAL-005). GLM (glm-4-flash) is available but exhibits ~3 first-attempt\n# JSON-parse failures per dual-kernel run, which can surface deterministic fallback text instead of\n# the model's real words -- so it is NOT the default for a demo. Override with DEMO_PROVIDER=glm.\nDEMO_PROVIDER=\"${DEMO_PROVIDER:-deepseek}\"\ncase \"$DEMO_PROVIDER\" in\n deepseek)\n if [ -z \"$DS_KEY\" ]; then echo \"ERROR: DEMO_PROVIDER=deepseek but no deepseek key in $KEYS.\" >&2; exit 1; fi\n CHAT_PROVIDER=\"deepseek\"; CHAT_KEY=\"$DS_KEY\" ;;\n glm)\n if [ -z \"$GLM_KEY\" ]; then echo \"ERROR: DEMO_PROVIDER=glm but no glm: line in $KEYS.\" >&2; exit 1; fi\n CHAT_PROVIDER=\"glm\"; CHAT_KEY=\"$GLM_KEY\"\n echo \"WARNING: GLM has known schema-drift (~3 fallbacks/run); DeepSeek is the recommended demo provider.\" >&2 ;;\n *) echo \"ERROR: DEMO_PROVIDER must be deepseek or glm (got: $DEMO_PROVIDER).\" >&2; exit 1 ;;\nesac\necho \"Loaded keys: qwen(len=${#QWEN_KEY}) ${DEMO_PROVIDER}(len=${#CHAT_KEY}) — values not printed.\"\n\n# Point embedding + TTS at this account's private Aliyun/DashScope gateway (from API及文档.txt).\nexport MEMORY_EMBEDDING_ENABLED=true\nexport MEMORY_EMBEDDING_API_KEY=\"$QWEN_KEY\"\nexport MEMORY_EMBEDDING_BASE_URL=\"https://llm-errus8cw2pf66bx9.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\nexport MEMORY_EMBEDDING_MODEL=\"text-embedding-v4\"\nexport TTS_ENABLED=true\nexport TTS_API_KEY=\"$QWEN_KEY\"\nexport TTS_WS_URL=\"wss://llm-errus8cw2pf66bx9.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference\"\n\n# Real chat provider (DeepSeek by default; GLM optional via DEMO_PROVIDER=glm), dev profile (H2).\nexport LLM_MODE=dev\nexport LLM_PROVIDER=\"$CHAT_PROVIDER\"\nexport GLM_API_KEY=\"$GLM_KEY\" # present in env regardless; only used if LLM_PROVIDER=glm\nexport DEEPSEEK_API_KEY=\"$DS_KEY\" # likewise; only used if LLM_PROVIDER=deepseek\nexport LLM_ALLOW_FALLBACK=false # a demo must show the REAL provider, never silently fall back to Mock\n\n# --- Public-tunnel hardening (2026-07-24 8-agent delivery-readiness audit, P1-4/P1-5/P1-7) ---\n# The whole point of this script is to be reachable over a public Cloudflare Tunnel (see\n# docs/demo/DEMO-RUNBOOK.md), so it must not run with plain-localhost-only defaults.\nexport COOKIE_SECURE=true # tunnel is HTTPS end-to-end; never ship an insecure cookie publicly\nexport COOKIE_SAME_SITE=none # the demo APK's WebView origin (https://localhost) is cross-site from\n # the tunnel origin; SameSite=Lax silently drops the session cookie there\nexport INNER_COSMOS_SECURITY_TRUSTED_PROXY_ENABLED=true # honor X-Forwarded-For from cloudflared so rate limits are per-visitor,\n # not one shared bucket for every judge hitting the tunnel from localhost\nexport MANAGEMENT_HEALTH_REDIS_ENABLED=false # this demo path never starts Redis; without this the health\n # endpoint falsely reports DOWN even though the app works fine\n# Never combine this demo path with SPRING_PROFILES_ACTIVE=demo/mysql or SEED_ENABLED=true: those\n# profiles default seed-enabled:true, which would put a hardcoded admin/admin123 account on the\n# public tunnel. This script intentionally never sets either.\n\n# JAVA_HOME for the Maven wrapper on Windows.\nif [ -z \"${JAVA_HOME:-}\" ] && [ -d \"/c/Program Files/Java/jdk-21.0.10\" ]; then\n export JAVA_HOME=\"/c/Program Files/Java/jdk-21.0.10\"\nfi\n\necho \"Starting Inner Cosmos on :${PORT} with LLM_PROVIDER=${DEMO_PROVIDER} + real embedding + real TTS...\"\necho \"Health check: http://localhost:${PORT}/actuator/health\"\necho \"App: http://localhost:${PORT}/app/aurora/\"\necho \"(Keep this terminal open. Stop with Ctrl+C.)\"\nexec ./mvnw.cmd spring-boot:run -Dspring-boot.run.jvmArguments=\"-Dserver.port=${PORT}\"\n"}, "e0175039c2dfe910bab73cfd8aa1269bf777a261": {"t": 1785758380.9934597, "n": 3740, "h": "6d0625346001106d", "c": "#!/usr/bin/env bash\n# Demo: start the Inner Cosmos backend on the laptop with REAL providers (GLM chat + Qwen embedding\n# + Qwen TTS), reading keys at runtime from the operator's gitignored API及文档.txt. Never writes a\n# key into any committed file. Use this for the in-class demo so the experience runs on real models.\n#\n# Usage: bash scripts/demo/run-demo-server.sh [PORT] (default 8080)\n# Env: DEMO_PORT, JAVA_HOME (defaults to C:\\Program Files\\Java\\jdk-21.0.10 on Windows)\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"$0\")/../..\" && pwd)\"\ncd \"$ROOT\"\nPORT=\"${DEMO_PORT:-${1:-8080}}\"\nKEYS=\"$ROOT/API及文档.txt\"\n\nif [ ! -f \"$KEYS\" ]; then\n echo \"ERROR: $KEYS not found. Put the operator's real keys there (it is gitignored).\" >&2\n exit 1\nfi\n\n# Extract keys without echoing the values (length only, for confirmation).\nQWEN_KEY=\"$(grep '^qwen:' \"$KEYS\" | head -1 | cut -d: -f2-)\"\nGLM_KEY=\"$(grep -i '^glm' \"$KEYS\" | head -1 | sed -E 's/^[Gg][Ll][Mm][::] *//')\"\nDS_KEY=\"$(grep -i 'deepseek' \"$KEYS\" | head -1 | sed -E 's/.*apikey[::] *//')\"\nif [ -z \"$QWEN_KEY\" ]; then\n echo \"ERROR: could not find the qwen: line in $KEYS (needed for embedding + TTS).\" >&2\n exit 1\nfi\n# Chat provider: DeepSeek by default (proven 0 schema-drift on this HEAD, see\n# evidence/innovation/INNO-EVAL-005). GLM (glm-4-flash) is available but exhibits ~3 first-attempt\n# JSON-parse failures per dual-kernel run, which can surface deterministic fallback text instead of\n# the model's real words -- so it is NOT the default for a demo. Override with DEMO_PROVIDER=glm.\nDEMO_PROVIDER=\"${DEMO_PROVIDER:-deepseek}\"\ncase \"$DEMO_PROVIDER\" in\n deepseek)\n if [ -z \"$DS_KEY\" ]; then echo \"ERROR: DEMO_PROVIDER=deepseek but no deepseek key in $KEYS.\" >&2; exit 1; fi\n CHAT_PROVIDER=\"deepseek\"; CHAT_KEY=\"$DS_KEY\" ;;\n glm)\n if [ -z \"$GLM_KEY\" ]; then echo \"ERROR: DEMO_PROVIDER=glm but no glm: line in $KEYS.\" >&2; exit 1; fi\n CHAT_PROVIDER=\"glm\"; CHAT_KEY=\"$GLM_KEY\"\n echo \"WARNING: GLM has known schema-drift (~3 fallbacks/run); DeepSeek is the recommended demo provider.\" >&2 ;;\n *) echo \"ERROR: DEMO_PROVIDER must be deepseek or glm (got: $DEMO_PROVIDER).\" >&2; exit 1 ;;\nesac\necho \"Loaded keys: qwen(len=${#QWEN_KEY}) ${DEMO_PROVIDER}(len=${#CHAT_KEY}) — values not printed.\"\n\n# Point embedding + TTS at this account's private Aliyun/DashScope gateway (from API及文档.txt).\nexport MEMORY_EMBEDDING_ENABLED=true\nexport MEMORY_EMBEDDING_API_KEY=\"$QWEN_KEY\"\nexport MEMORY_EMBEDDING_BASE_URL=\"https://llm-errus8cw2pf66bx9.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\nexport MEMORY_EMBEDDING_MODEL=\"text-embedding-v4\"\nexport TTS_ENABLED=true\nexport TTS_API_KEY=\"$QWEN_KEY\"\nexport TTS_WS_URL=\"wss://llm-errus8cw2pf66bx9.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference\"\n\n# Real chat provider (DeepSeek by default; GLM optional via DEMO_PROVIDER=glm), dev profile (H2).\nexport LLM_MODE=dev\nexport LLM_PROVIDER=\"$CHAT_PROVIDER\"\nexport GLM_API_KEY=\"$GLM_KEY\" # present in env regardless; only used if LLM_PROVIDER=glm\nexport DEEPSEEK_API_KEY=\"$DS_KEY\" # likewise; only used if LLM_PROVIDER=deepseek\nexport LLM_ALLOW_FALLBACK=false # a demo must show the REAL provider, never silently fall back to Mock\n\n# JAVA_HOME for the Maven wrapper on Windows.\nif [ -z \"${JAVA_HOME:-}\" ] && [ -d \"/c/Program Files/Java/jdk-21.0.10\" ]; then\n export JAVA_HOME=\"/c/Program Files/Java/jdk-21.0.10\"\nfi\n\necho \"Starting Inner Cosmos on :${PORT} with LLM_PROVIDER=glm + real embedding + real TTS...\"\necho \"Health check: http://localhost:${PORT}/actuator/health\"\necho \"App: http://localhost:${PORT}/app/aurora/\"\necho \"(Keep this terminal open. Stop with Ctrl+C.)\"\nexec ./mvnw.cmd spring-boot:run -Dspring-boot.run.jvmArguments=\"-Dserver.port=${PORT}\"\n"}, "7b04498db8f17459bda4c2a398056d62c47bafd3": {"t": 1785758382.3681655, "n": 2382, "h": "5e441f37b7550cce", "c": "#!/usr/bin/env bash\n# Demo: start the Inner Cosmos backend on the laptop with REAL providers (GLM chat + Qwen embedding\n# + Qwen TTS), reading keys at runtime from the operator's gitignored API及文档.txt. Never writes a\n# key into any committed file. Use this for the in-class demo so the experience runs on real models.\n#\n# Usage: bash scripts/demo/run-demo-server.sh [PORT] (default 8080)\n# Env: DEMO_PORT, JAVA_HOME (defaults to C:\\Program Files\\Java\\jdk-21.0.10 on Windows)\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"$0\")/../..\" && pwd)\"\ncd \"$ROOT\"\nPORT=\"${DEMO_PORT:-${1:-8080}}\"\nKEYS=\"$ROOT/API及文档.txt\"\n\nif [ ! -f \"$KEYS\" ]; then\n echo \"ERROR: $KEYS not found. Put the operator's real keys there (it is gitignored).\" >&2\n exit 1\nfi\n\n# Extract keys without echoing the values (length only, for confirmation).\nQWEN_KEY=\"$(grep '^qwen:' \"$KEYS\" | head -1 | cut -d: -f2-)\"\nGLM_KEY=\"$(grep -i '^glm' \"$KEYS\" | head -1 | sed -E 's/^[Gg][Ll][Mm][::] *//')\"\nif [ -z \"$QWEN_KEY\" ] || [ -z \"$GLM_KEY\" ]; then\n echo \"ERROR: could not find the qwen: and glm: lines in $KEYS.\" >&2\n exit 1\nfi\necho \"Loaded keys: qwen(len=${#QWEN_KEY}) glm(len=${#GLM_KEY}) — values not printed.\"\n\n# Point embedding + TTS at this account's private Aliyun/DashScope gateway (from API及文档.txt).\nexport MEMORY_EMBEDDING_ENABLED=true\nexport MEMORY_EMBEDDING_API_KEY=\"$QWEN_KEY\"\nexport MEMORY_EMBEDDING_BASE_URL=\"https://llm-errus8cw2pf66bx9.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\nexport MEMORY_EMBEDDING_MODEL=\"text-embedding-v4\"\nexport TTS_ENABLED=true\nexport TTS_API_KEY=\"$QWEN_KEY\"\nexport TTS_WS_URL=\"wss://llm-errus8cw2pf66bx9.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference\"\n\n# Real GLM chat provider (the project's default failover chain), dev profile (H2), on the chosen port.\nexport LLM_MODE=dev\nexport LLM_PROVIDER=glm\nexport GLM_API_KEY=\"$GLM_KEY\"\n\n# JAVA_HOME for the Maven wrapper on Windows.\nif [ -z \"${JAVA_HOME:-}\" ] && [ -d \"/c/Program Files/Java/jdk-21.0.10\" ]; then\n export JAVA_HOME=\"/c/Program Files/Java/jdk-21.0.10\"\nfi\n\necho \"Starting Inner Cosmos on :${PORT} with LLM_PROVIDER=glm + real embedding + real TTS...\"\necho \"Health check: http://localhost:${PORT}/actuator/health\"\necho \"App: http://localhost:${PORT}/app/aurora/\"\necho \"(Keep this terminal open. Stop with Ctrl+C.)\"\nexec ./mvnw.cmd spring-boot:run -Dspring-boot.run.jvmArguments=\"-Dserver.port=${PORT}\"\n"}}, "src/main/java/com/innercosmos/ai/client/DeepSeekLlmClient.java": {"add4b241338c47dd1ccbafa2880f7b484adb07b0": {"t": 1785746128.9898458, "n": 14156, "h": "438a2ad82e700b29", "c": "package com.innercosmos.ai.client;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.innercosmos.service.AiLogService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Executor;\n\npublic class DeepSeekLlmClient implements LlmClient {\n private static final Logger log = LoggerFactory.getLogger(DeepSeekLlmClient.class);\n private static final ObjectMapper objectMapper = new ObjectMapper();\n\n private final String apiKey;\n private final String baseUrl;\n private final String model;\n private final int timeoutMs;\n private final boolean allowFallback;\n private final AiLogService aiLogService;\n private final Executor aiExecutor;\n private final MockLlmClient fallback;\n private final HttpClient httpClient;\n\n public DeepSeekLlmClient(String apiKey, String baseUrl, String model, int timeoutMs,\n boolean allowFallback, AiLogService aiLogService, Executor aiExecutor) {\n this.apiKey = apiKey;\n this.baseUrl = normalizeUrl(baseUrl);\n this.model = model == null || model.isBlank() ? \"deepseek-v4-flash\" : model;\n this.timeoutMs = timeoutMs <= 0 ? 30000 : timeoutMs;\n this.allowFallback = allowFallback;\n this.aiLogService = aiLogService;\n this.aiExecutor = aiExecutor;\n this.fallback = new MockLlmClient(aiExecutor);\n this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(20)).build();\n }\n\n @Override\n public String chat(LlmRequest request) {\n long start = System.currentTimeMillis();\n String response = null;\n boolean success = false;\n boolean fallbackUsed = false;\n String errorMessage = null;\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception firstError) {\n errorMessage = firstError.getMessage();\n if (!request.retryEnabledOrDefault()) {\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed within the stage deadline and retry is disabled\",\n firstError);\n }\n log.warn(\"DeepSeek stage failed without retry, falling back to local mock: {}\", firstError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n log.warn(\"DeepSeek chat failed on first attempt, retrying once: {}\", firstError.getMessage());\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw firstError;\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception retryError) {\n errorMessage = retryError.getMessage();\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed and fallback is disabled\", retryError);\n }\n log.error(\"DeepSeek chat failed on retry, falling back to local mock: {}\", retryError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n } finally {\n aiLogService.recordDetailed(request.userId, request.moduleName, fallbackUsed ? \"MOCK\" : \"DEEPSEEK\",\n fallbackUsed ? \"mock-inner-cosmos\" : model, request.prompt, response, request.requestJson,\n response, success, fallbackUsed, fallbackUsed ? errorMessage : (success ? null : errorMessage),\n System.currentTimeMillis() - start);\n }\n }\n\n @Override\n public SseEmitter streamChat(LlmRequest request) {\n SseEmitter emitter = new SseEmitter(120_000L);\n aiExecutor.execute(() -> {\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n String aggregated = streamRemote(request, emitter);\n if (aggregated == null) {\n return;\n }\n emitter.send(SseEmitter.event().name(\"done\").data(\"{\\\"message\\\":\\\"done\\\"}\"));\n emitter.complete();\n } catch (Exception exception) {\n emitter.completeWithError(exception);\n }\n });\n return emitter;\n }\n\n /**\n * Real provider SSE streaming (VS-003 §2). DeepSeek is OpenAI-compatible.\n */\n private String streamRemote(LlmRequest request, SseEmitter emitter) throws Exception {\n List<Map<String, String>> messages = buildMessages(request);\n Map<String, Object> body = requestBody(request, messages, true);\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"Accept\", \"text/event-stream\")\n .timeout(Duration.ofMillis(request.timeoutMsOr(timeoutMs)))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n StringBuilder aggregated = new StringBuilder();\n try {\n HttpResponse<java.util.stream.Stream<String>> httpResponse = httpClient.send(httpRequest,\n HttpResponse.BodyHandlers.ofLines());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n // Non-2xx (rate limit / bad request / auth): body is an error JSON, not SSE `data:`\n // lines. Without this guard those lines are all skipped, the stream ends empty, and the\n // caller emits a silent empty `done` -> user sees no reply. Route it into the fallback.\n httpResponse.body().close();\n throw new java.io.IOException(\"DeepSeek stream HTTP \" + httpResponse.statusCode());\n }\n httpResponse.body().forEach(line -> {\n if (line == null || !line.startsWith(\"data:\")) return;\n String payload = line.substring(5).trim();\n if (payload.isEmpty() || \"[DONE]\".equals(payload)) return;\n try {\n JsonNode delta = objectMapper.readTree(payload)\n .path(\"choices\").path(0).path(\"delta\").path(\"content\");\n if (!delta.isMissingNode() && !delta.asText().isEmpty()) {\n String token = delta.asText();\n aggregated.append(token);\n emitter.send(SseEmitter.event().name(\"token\")\n .data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n } catch (Exception parseEx) {\n log.debug(\"DeepSeek stream parse skip: {}\", parseEx.getMessage());\n }\n });\n } catch (Exception remoteError) {\n log.warn(\"DeepSeek stream failed, falling back to chat drip: {}\", remoteError.getMessage());\n // Only drip the full fallback when nothing was streamed yet, otherwise we would duplicate\n // the partial content already delivered to the client.\n if (aggregated.length() == 0) {\n return dripFromChat(request, emitter);\n }\n return aggregated.toString();\n }\n if (aggregated.length() == 0) {\n // 2xx but no content deltas (empty/malformed stream) -> fall back so the turn still replies.\n log.warn(\"DeepSeek stream returned empty content, falling back to chat drip\");\n return dripFromChat(request, emitter);\n }\n return aggregated.toString();\n }\n\n /** Blocking-path fallback (retries then local mock) dripped out as stream tokens. */\n private String dripFromChat(LlmRequest request, SseEmitter emitter) throws Exception {\n String full = chat(request);\n for (String token : full.split(\"\")) {\n emitter.send(SseEmitter.event().name(\"token\").data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n return full;\n }\n\n private List<Map<String, String>> buildMessages(LlmRequest request) {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", request.systemPromptOr(systemPrompt())));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n return messages;\n }\n\n private String doChat(LlmRequest request) throws Exception {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", request.systemPromptOr(systemPrompt())));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n Map<String, Object> body = requestBody(request, messages, false);\n\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .timeout(Duration.ofMillis(request.timeoutMsOr(timeoutMs)))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n throw new RuntimeException(\"DeepSeek API returned status \" + httpResponse.statusCode() + \": \" + httpResponse.body());\n }\n JsonNode root = objectMapper.readTree(httpResponse.body());\n JsonNode choice = root.path(\"choices\").path(0);\n JsonNode message = choice.path(\"message\");\n JsonNode content = message.path(\"content\");\n String finishReason = choice.path(\"finish_reason\").asText(\"\");\n boolean reasoningPresent = !message.path(\"reasoning_content\").asText(\"\").isBlank();\n int completionTokens = root.path(\"usage\").path(\"completion_tokens\").asInt(-1);\n if (\"length\".equalsIgnoreCase(finishReason)) {\n throw new RuntimeException(\"DeepSeek completion exhausted output budget\"\n + \" (finish_reason=length, completion_tokens=\" + completionTokens\n + \", content_blank=\" + content.asText(\"\").isBlank()\n + \", reasoning_content_present=\" + reasoningPresent + \")\");\n }\n if (content.isMissingNode() || content.asText().isBlank()) {\n throw new RuntimeException(\"DeepSeek completion contained no usable content\"\n + \" (finish_reason=\" + (finishReason.isBlank() ? \"unknown\" : finishReason)\n + \", completion_tokens=\" + completionTokens\n + \", reasoning_content_present=\" + reasoningPresent + \")\");\n }\n return content.asText();\n }\n\n Map<String, Object> requestBody(LlmRequest request, List<Map<String, String>> messages, boolean stream) {\n boolean thinking = Boolean.TRUE.equals(request.thinkingEnabled);\n Map<String, Object> body = new LinkedHashMap<>();\n body.put(\"model\", model);\n body.put(\"messages\", messages);\n body.put(\"max_tokens\", request.maxTokensOr(LlmClient.RESPONSE_MAX_TOKENS));\n body.put(\"stream\", stream);\n body.put(\"thinking\", Map.of(\"type\", thinking ? \"enabled\" : \"disabled\"));\n if (thinking && request.reasoningEffort != null && !request.reasoningEffort.isBlank()) {\n body.put(\"reasoning_effort\", request.reasoningEffort.trim());\n }\n // DeepSeek thinking mode does not support temperature. Omitting it avoids a misleading\n // no-op parameter while preserving the foreground speaker's tuned sampling.\n if (!thinking) {\n body.put(\"temperature\", request.temperature != null ? request.temperature : 0.72);\n }\n return body;\n }\n\n private String systemPrompt() {\n return \"You are Aurora in Inner Cosmos. Be warm, specific, safe, and reflective. \"\n + \"Use the user's language. Ask at most one question. Never diagnose.\";\n }\n\n private String normalizeUrl(String value) {\n if (value == null || value.isBlank()) return \"https://api.deepseek.com/chat/completions\";\n if (value.endsWith(\"/chat/completions\")) return value;\n return value.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n\n private String escape(String token) {\n return token.replace(\"\\\\\", \"\\\\\\\\\")\n .replace(\"\\\"\", \"\\\\\\\"\")\n .replace(\"\\n\", \"\\\\n\")\n .replace(\"\\r\", \"\\\\r\")\n .replace(\"\\t\", \"\\\\t\");\n }\n}\n"}, "ead110236893cf89e66f97f6619243f43f2fc883": {"t": 1785758373.9649189, "n": 14156, "h": "438a2ad82e700b29", "c": "package com.innercosmos.ai.client;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.innercosmos.service.AiLogService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Executor;\n\npublic class DeepSeekLlmClient implements LlmClient {\n private static final Logger log = LoggerFactory.getLogger(DeepSeekLlmClient.class);\n private static final ObjectMapper objectMapper = new ObjectMapper();\n\n private final String apiKey;\n private final String baseUrl;\n private final String model;\n private final int timeoutMs;\n private final boolean allowFallback;\n private final AiLogService aiLogService;\n private final Executor aiExecutor;\n private final MockLlmClient fallback;\n private final HttpClient httpClient;\n\n public DeepSeekLlmClient(String apiKey, String baseUrl, String model, int timeoutMs,\n boolean allowFallback, AiLogService aiLogService, Executor aiExecutor) {\n this.apiKey = apiKey;\n this.baseUrl = normalizeUrl(baseUrl);\n this.model = model == null || model.isBlank() ? \"deepseek-v4-flash\" : model;\n this.timeoutMs = timeoutMs <= 0 ? 30000 : timeoutMs;\n this.allowFallback = allowFallback;\n this.aiLogService = aiLogService;\n this.aiExecutor = aiExecutor;\n this.fallback = new MockLlmClient(aiExecutor);\n this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(20)).build();\n }\n\n @Override\n public String chat(LlmRequest request) {\n long start = System.currentTimeMillis();\n String response = null;\n boolean success = false;\n boolean fallbackUsed = false;\n String errorMessage = null;\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception firstError) {\n errorMessage = firstError.getMessage();\n if (!request.retryEnabledOrDefault()) {\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed within the stage deadline and retry is disabled\",\n firstError);\n }\n log.warn(\"DeepSeek stage failed without retry, falling back to local mock: {}\", firstError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n log.warn(\"DeepSeek chat failed on first attempt, retrying once: {}\", firstError.getMessage());\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw firstError;\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception retryError) {\n errorMessage = retryError.getMessage();\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed and fallback is disabled\", retryError);\n }\n log.error(\"DeepSeek chat failed on retry, falling back to local mock: {}\", retryError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n } finally {\n aiLogService.recordDetailed(request.userId, request.moduleName, fallbackUsed ? \"MOCK\" : \"DEEPSEEK\",\n fallbackUsed ? \"mock-inner-cosmos\" : model, request.prompt, response, request.requestJson,\n response, success, fallbackUsed, fallbackUsed ? errorMessage : (success ? null : errorMessage),\n System.currentTimeMillis() - start);\n }\n }\n\n @Override\n public SseEmitter streamChat(LlmRequest request) {\n SseEmitter emitter = new SseEmitter(120_000L);\n aiExecutor.execute(() -> {\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n String aggregated = streamRemote(request, emitter);\n if (aggregated == null) {\n return;\n }\n emitter.send(SseEmitter.event().name(\"done\").data(\"{\\\"message\\\":\\\"done\\\"}\"));\n emitter.complete();\n } catch (Exception exception) {\n emitter.completeWithError(exception);\n }\n });\n return emitter;\n }\n\n /**\n * Real provider SSE streaming (VS-003 §2). DeepSeek is OpenAI-compatible.\n */\n private String streamRemote(LlmRequest request, SseEmitter emitter) throws Exception {\n List<Map<String, String>> messages = buildMessages(request);\n Map<String, Object> body = requestBody(request, messages, true);\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"Accept\", \"text/event-stream\")\n .timeout(Duration.ofMillis(request.timeoutMsOr(timeoutMs)))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n StringBuilder aggregated = new StringBuilder();\n try {\n HttpResponse<java.util.stream.Stream<String>> httpResponse = httpClient.send(httpRequest,\n HttpResponse.BodyHandlers.ofLines());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n // Non-2xx (rate limit / bad request / auth): body is an error JSON, not SSE `data:`\n // lines. Without this guard those lines are all skipped, the stream ends empty, and the\n // caller emits a silent empty `done` -> user sees no reply. Route it into the fallback.\n httpResponse.body().close();\n throw new java.io.IOException(\"DeepSeek stream HTTP \" + httpResponse.statusCode());\n }\n httpResponse.body().forEach(line -> {\n if (line == null || !line.startsWith(\"data:\")) return;\n String payload = line.substring(5).trim();\n if (payload.isEmpty() || \"[DONE]\".equals(payload)) return;\n try {\n JsonNode delta = objectMapper.readTree(payload)\n .path(\"choices\").path(0).path(\"delta\").path(\"content\");\n if (!delta.isMissingNode() && !delta.asText().isEmpty()) {\n String token = delta.asText();\n aggregated.append(token);\n emitter.send(SseEmitter.event().name(\"token\")\n .data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n } catch (Exception parseEx) {\n log.debug(\"DeepSeek stream parse skip: {}\", parseEx.getMessage());\n }\n });\n } catch (Exception remoteError) {\n log.warn(\"DeepSeek stream failed, falling back to chat drip: {}\", remoteError.getMessage());\n // Only drip the full fallback when nothing was streamed yet, otherwise we would duplicate\n // the partial content already delivered to the client.\n if (aggregated.length() == 0) {\n return dripFromChat(request, emitter);\n }\n return aggregated.toString();\n }\n if (aggregated.length() == 0) {\n // 2xx but no content deltas (empty/malformed stream) -> fall back so the turn still replies.\n log.warn(\"DeepSeek stream returned empty content, falling back to chat drip\");\n return dripFromChat(request, emitter);\n }\n return aggregated.toString();\n }\n\n /** Blocking-path fallback (retries then local mock) dripped out as stream tokens. */\n private String dripFromChat(LlmRequest request, SseEmitter emitter) throws Exception {\n String full = chat(request);\n for (String token : full.split(\"\")) {\n emitter.send(SseEmitter.event().name(\"token\").data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n return full;\n }\n\n private List<Map<String, String>> buildMessages(LlmRequest request) {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", request.systemPromptOr(systemPrompt())));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n return messages;\n }\n\n private String doChat(LlmRequest request) throws Exception {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", request.systemPromptOr(systemPrompt())));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n Map<String, Object> body = requestBody(request, messages, false);\n\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .timeout(Duration.ofMillis(request.timeoutMsOr(timeoutMs)))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n throw new RuntimeException(\"DeepSeek API returned status \" + httpResponse.statusCode() + \": \" + httpResponse.body());\n }\n JsonNode root = objectMapper.readTree(httpResponse.body());\n JsonNode choice = root.path(\"choices\").path(0);\n JsonNode message = choice.path(\"message\");\n JsonNode content = message.path(\"content\");\n String finishReason = choice.path(\"finish_reason\").asText(\"\");\n boolean reasoningPresent = !message.path(\"reasoning_content\").asText(\"\").isBlank();\n int completionTokens = root.path(\"usage\").path(\"completion_tokens\").asInt(-1);\n if (\"length\".equalsIgnoreCase(finishReason)) {\n throw new RuntimeException(\"DeepSeek completion exhausted output budget\"\n + \" (finish_reason=length, completion_tokens=\" + completionTokens\n + \", content_blank=\" + content.asText(\"\").isBlank()\n + \", reasoning_content_present=\" + reasoningPresent + \")\");\n }\n if (content.isMissingNode() || content.asText().isBlank()) {\n throw new RuntimeException(\"DeepSeek completion contained no usable content\"\n + \" (finish_reason=\" + (finishReason.isBlank() ? \"unknown\" : finishReason)\n + \", completion_tokens=\" + completionTokens\n + \", reasoning_content_present=\" + reasoningPresent + \")\");\n }\n return content.asText();\n }\n\n Map<String, Object> requestBody(LlmRequest request, List<Map<String, String>> messages, boolean stream) {\n boolean thinking = Boolean.TRUE.equals(request.thinkingEnabled);\n Map<String, Object> body = new LinkedHashMap<>();\n body.put(\"model\", model);\n body.put(\"messages\", messages);\n body.put(\"max_tokens\", request.maxTokensOr(LlmClient.RESPONSE_MAX_TOKENS));\n body.put(\"stream\", stream);\n body.put(\"thinking\", Map.of(\"type\", thinking ? \"enabled\" : \"disabled\"));\n if (thinking && request.reasoningEffort != null && !request.reasoningEffort.isBlank()) {\n body.put(\"reasoning_effort\", request.reasoningEffort.trim());\n }\n // DeepSeek thinking mode does not support temperature. Omitting it avoids a misleading\n // no-op parameter while preserving the foreground speaker's tuned sampling.\n if (!thinking) {\n body.put(\"temperature\", request.temperature != null ? request.temperature : 0.72);\n }\n return body;\n }\n\n private String systemPrompt() {\n return \"You are Aurora in Inner Cosmos. Be warm, specific, safe, and reflective. \"\n + \"Use the user's language. Ask at most one question. Never diagnose.\";\n }\n\n private String normalizeUrl(String value) {\n if (value == null || value.isBlank()) return \"https://api.deepseek.com/chat/completions\";\n if (value.endsWith(\"/chat/completions\")) return value;\n return value.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n\n private String escape(String token) {\n return token.replace(\"\\\\\", \"\\\\\\\\\")\n .replace(\"\\\"\", \"\\\\\\\"\")\n .replace(\"\\n\", \"\\\\n\")\n .replace(\"\\r\", \"\\\\r\")\n .replace(\"\\t\", \"\\\\t\");\n }\n}\n"}, "946cc55d547dc78388a6eb7f4fa8e73966b4d09e": {"t": 1785758375.4808555, "n": 13042, "h": "ada2db40129ac917", "c": "package com.innercosmos.ai.client;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.innercosmos.service.AiLogService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Executor;\n\npublic class DeepSeekLlmClient implements LlmClient {\n private static final Logger log = LoggerFactory.getLogger(DeepSeekLlmClient.class);\n private static final ObjectMapper objectMapper = new ObjectMapper();\n\n private final String apiKey;\n private final String baseUrl;\n private final String model;\n private final int timeoutMs;\n private final boolean allowFallback;\n private final AiLogService aiLogService;\n private final Executor aiExecutor;\n private final MockLlmClient fallback;\n private final HttpClient httpClient;\n\n public DeepSeekLlmClient(String apiKey, String baseUrl, String model, int timeoutMs,\n boolean allowFallback, AiLogService aiLogService, Executor aiExecutor) {\n this.apiKey = apiKey;\n this.baseUrl = normalizeUrl(baseUrl);\n this.model = model == null || model.isBlank() ? \"deepseek-v4-flash\" : model;\n this.timeoutMs = timeoutMs <= 0 ? 30000 : timeoutMs;\n this.allowFallback = allowFallback;\n this.aiLogService = aiLogService;\n this.aiExecutor = aiExecutor;\n this.fallback = new MockLlmClient(aiExecutor);\n this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(20)).build();\n }\n\n @Override\n public String chat(LlmRequest request) {\n long start = System.currentTimeMillis();\n String response = null;\n boolean success = false;\n boolean fallbackUsed = false;\n String errorMessage = null;\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception firstError) {\n errorMessage = firstError.getMessage();\n if (!request.retryEnabledOrDefault()) {\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed within the stage deadline and retry is disabled\",\n firstError);\n }\n log.warn(\"DeepSeek stage failed without retry, falling back to local mock: {}\", firstError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n log.warn(\"DeepSeek chat failed on first attempt, retrying once: {}\", firstError.getMessage());\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw firstError;\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception retryError) {\n errorMessage = retryError.getMessage();\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed and fallback is disabled\", retryError);\n }\n log.error(\"DeepSeek chat failed on retry, falling back to local mock: {}\", retryError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n } finally {\n aiLogService.recordDetailed(request.userId, request.moduleName, fallbackUsed ? \"MOCK\" : \"DEEPSEEK\",\n fallbackUsed ? \"mock-inner-cosmos\" : model, request.prompt, response, request.requestJson,\n response, success, fallbackUsed, fallbackUsed ? errorMessage : (success ? null : errorMessage),\n System.currentTimeMillis() - start);\n }\n }\n\n @Override\n public SseEmitter streamChat(LlmRequest request) {\n SseEmitter emitter = new SseEmitter(120_000L);\n aiExecutor.execute(() -> {\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n String aggregated = streamRemote(request, emitter);\n if (aggregated == null) {\n return;\n }\n emitter.send(SseEmitter.event().name(\"done\").data(\"{\\\"message\\\":\\\"done\\\"}\"));\n emitter.complete();\n } catch (Exception exception) {\n emitter.completeWithError(exception);\n }\n });\n return emitter;\n }\n\n /**\n * Real provider SSE streaming (VS-003 §2). DeepSeek is OpenAI-compatible.\n */\n private String streamRemote(LlmRequest request, SseEmitter emitter) throws Exception {\n List<Map<String, String>> messages = buildMessages(request);\n Map<String, Object> body = requestBody(request, messages, true);\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"Accept\", \"text/event-stream\")\n .timeout(Duration.ofMillis(request.timeoutMsOr(timeoutMs)))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n StringBuilder aggregated = new StringBuilder();\n try {\n HttpResponse<java.util.stream.Stream<String>> httpResponse = httpClient.send(httpRequest,\n HttpResponse.BodyHandlers.ofLines());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n // Non-2xx (rate limit / bad request / auth): body is an error JSON, not SSE `data:`\n // lines. Without this guard those lines are all skipped, the stream ends empty, and the\n // caller emits a silent empty `done` -> user sees no reply. Route it into the fallback.\n httpResponse.body().close();\n throw new java.io.IOException(\"DeepSeek stream HTTP \" + httpResponse.statusCode());\n }\n httpResponse.body().forEach(line -> {\n if (line == null || !line.startsWith(\"data:\")) return;\n String payload = line.substring(5).trim();\n if (payload.isEmpty() || \"[DONE]\".equals(payload)) return;\n try {\n JsonNode delta = objectMapper.readTree(payload)\n .path(\"choices\").path(0).path(\"delta\").path(\"content\");\n if (!delta.isMissingNode() && !delta.asText().isEmpty()) {\n String token = delta.asText();\n aggregated.append(token);\n emitter.send(SseEmitter.event().name(\"token\")\n .data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n } catch (Exception parseEx) {\n log.debug(\"DeepSeek stream parse skip: {}\", parseEx.getMessage());\n }\n });\n } catch (Exception remoteError) {\n log.warn(\"DeepSeek stream failed, falling back to chat drip: {}\", remoteError.getMessage());\n // Only drip the full fallback when nothing was streamed yet, otherwise we would duplicate\n // the partial content already delivered to the client.\n if (aggregated.length() == 0) {\n return dripFromChat(request, emitter);\n }\n return aggregated.toString();\n }\n if (aggregated.length() == 0) {\n // 2xx but no content deltas (empty/malformed stream) -> fall back so the turn still replies.\n log.warn(\"DeepSeek stream returned empty content, falling back to chat drip\");\n return dripFromChat(request, emitter);\n }\n return aggregated.toString();\n }\n\n /** Blocking-path fallback (retries then local mock) dripped out as stream tokens. */\n private String dripFromChat(LlmRequest request, SseEmitter emitter) throws Exception {\n String full = chat(request);\n for (String token : full.split(\"\")) {\n emitter.send(SseEmitter.event().name(\"token\").data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n return full;\n }\n\n private List<Map<String, String>> buildMessages(LlmRequest request) {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", request.systemPromptOr(systemPrompt())));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n return messages;\n }\n\n private String doChat(LlmRequest request) throws Exception {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", request.systemPromptOr(systemPrompt())));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n Map<String, Object> body = requestBody(request, messages, false);\n\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .timeout(Duration.ofMillis(request.timeoutMsOr(timeoutMs)))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n throw new RuntimeException(\"DeepSeek API returned status \" + httpResponse.statusCode() + \": \" + httpResponse.body());\n }\n JsonNode root = objectMapper.readTree(httpResponse.body());\n JsonNode content = root.path(\"choices\").path(0).path(\"message\").path(\"content\");\n if (content.isMissingNode() || content.asText().isBlank()) {\n throw new RuntimeException(\"Unexpected DeepSeek response format: \" + httpResponse.body());\n }\n return content.asText();\n }\n\n Map<String, Object> requestBody(LlmRequest request, List<Map<String, String>> messages, boolean stream) {\n boolean thinking = Boolean.TRUE.equals(request.thinkingEnabled);\n Map<String, Object> body = new LinkedHashMap<>();\n body.put(\"model\", model);\n body.put(\"messages\", messages);\n body.put(\"max_tokens\", request.maxTokensOr(LlmClient.RESPONSE_MAX_TOKENS));\n body.put(\"stream\", stream);\n body.put(\"thinking\", Map.of(\"type\", thinking ? \"enabled\" : \"disabled\"));\n // DeepSeek thinking mode does not support temperature. Omitting it avoids a misleading\n // no-op parameter while preserving the foreground speaker's tuned sampling.\n if (!thinking) {\n body.put(\"temperature\", request.temperature != null ? request.temperature : 0.72);\n }\n return body;\n }\n\n private String systemPrompt() {\n return \"You are Aurora in Inner Cosmos. Be warm, specific, safe, and reflective. \"\n + \"Use the user's language. Ask at most one question. Never diagnose.\";\n }\n\n private String normalizeUrl(String value) {\n if (value == null || value.isBlank()) return \"https://api.deepseek.com/chat/completions\";\n if (value.endsWith(\"/chat/completions\")) return value;\n return value.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n\n private String escape(String token) {\n return token.replace(\"\\\\\", \"\\\\\\\\\")\n .replace(\"\\\"\", \"\\\\\\\"\")\n .replace(\"\\n\", \"\\\\n\")\n .replace(\"\\r\", \"\\\\r\")\n .replace(\"\\t\", \"\\\\t\");\n }\n}\n"}, "4863de92bc356c4f32fa16bf19b4d27b735957e3": {"t": 1785758377.4656754, "n": 11994, "h": "78e6bf573949dbe6", "c": "package com.innercosmos.ai.client;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.innercosmos.service.AiLogService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Executor;\n\npublic class DeepSeekLlmClient implements LlmClient {\n private static final Logger log = LoggerFactory.getLogger(DeepSeekLlmClient.class);\n private static final ObjectMapper objectMapper = new ObjectMapper();\n\n private final String apiKey;\n private final String baseUrl;\n private final String model;\n private final int timeoutMs;\n private final boolean allowFallback;\n private final AiLogService aiLogService;\n private final Executor aiExecutor;\n private final MockLlmClient fallback;\n private final HttpClient httpClient;\n\n public DeepSeekLlmClient(String apiKey, String baseUrl, String model, int timeoutMs,\n boolean allowFallback, AiLogService aiLogService, Executor aiExecutor) {\n this.apiKey = apiKey;\n this.baseUrl = normalizeUrl(baseUrl);\n this.model = model == null || model.isBlank() ? \"deepseek-chat\" : model;\n this.timeoutMs = timeoutMs <= 0 ? 30000 : timeoutMs;\n this.allowFallback = allowFallback;\n this.aiLogService = aiLogService;\n this.aiExecutor = aiExecutor;\n this.fallback = new MockLlmClient(aiExecutor);\n this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(20)).build();\n }\n\n @Override\n public String chat(LlmRequest request) {\n long start = System.currentTimeMillis();\n String response = null;\n boolean success = false;\n boolean fallbackUsed = false;\n String errorMessage = null;\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception firstError) {\n errorMessage = firstError.getMessage();\n log.warn(\"DeepSeek chat failed on first attempt: {}\", firstError.getMessage());\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw firstError;\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception retryError) {\n errorMessage = retryError.getMessage();\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed and fallback is disabled\", retryError);\n }\n log.error(\"DeepSeek chat failed on retry, falling back to local mock: {}\", retryError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n } finally {\n aiLogService.recordDetailed(request.userId, request.moduleName, fallbackUsed ? \"MOCK\" : \"DEEPSEEK\",\n fallbackUsed ? \"mock-inner-cosmos\" : model, request.prompt, response, request.requestJson,\n response, success, fallbackUsed, fallbackUsed ? errorMessage : (success ? null : errorMessage),\n System.currentTimeMillis() - start);\n }\n }\n\n @Override\n public SseEmitter streamChat(LlmRequest request) {\n SseEmitter emitter = new SseEmitter(120_000L);\n aiExecutor.execute(() -> {\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n String aggregated = streamRemote(request, emitter);\n if (aggregated == null) {\n return;\n }\n emitter.send(SseEmitter.event().name(\"done\").data(\"{\\\"message\\\":\\\"done\\\"}\"));\n emitter.complete();\n } catch (Exception exception) {\n emitter.completeWithError(exception);\n }\n });\n return emitter;\n }\n\n /**\n * Real provider SSE streaming (VS-003 §2). DeepSeek is OpenAI-compatible.\n */\n private String streamRemote(LlmRequest request, SseEmitter emitter) throws Exception {\n List<Map<String, String>> messages = buildMessages(request);\n Map<String, Object> body = Map.of(\n \"model\", model,\n \"messages\", messages,\n \"temperature\", request.temperature != null ? request.temperature : 0.72,\n \"max_tokens\", LlmClient.RESPONSE_MAX_TOKENS,\n \"stream\", true\n );\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"Accept\", \"text/event-stream\")\n .timeout(Duration.ofMillis(timeoutMs))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n StringBuilder aggregated = new StringBuilder();\n try {\n HttpResponse<java.util.stream.Stream<String>> httpResponse = httpClient.send(httpRequest,\n HttpResponse.BodyHandlers.ofLines());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n // Non-2xx (rate limit / bad request / auth): body is an error JSON, not SSE `data:`\n // lines. Without this guard those lines are all skipped, the stream ends empty, and the\n // caller emits a silent empty `done` -> user sees no reply. Route it into the fallback.\n httpResponse.body().close();\n throw new java.io.IOException(\"DeepSeek stream HTTP \" + httpResponse.statusCode());\n }\n httpResponse.body().forEach(line -> {\n if (line == null || !line.startsWith(\"data:\")) return;\n String payload = line.substring(5).trim();\n if (payload.isEmpty() || \"[DONE]\".equals(payload)) return;\n try {\n JsonNode delta = objectMapper.readTree(payload)\n .path(\"choices\").path(0).path(\"delta\").path(\"content\");\n if (!delta.isMissingNode() && !delta.asText().isEmpty()) {\n String token = delta.asText();\n aggregated.append(token);\n emitter.send(SseEmitter.event().name(\"token\")\n .data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n } catch (Exception parseEx) {\n log.debug(\"DeepSeek stream parse skip: {}\", parseEx.getMessage());\n }\n });\n } catch (Exception remoteError) {\n log.warn(\"DeepSeek stream failed, falling back to chat drip: {}\", remoteError.getMessage());\n // Only drip the full fallback when nothing was streamed yet, otherwise we would duplicate\n // the partial content already delivered to the client.\n if (aggregated.length() == 0) {\n return dripFromChat(request, emitter);\n }\n return aggregated.toString();\n }\n if (aggregated.length() == 0) {\n // 2xx but no content deltas (empty/malformed stream) -> fall back so the turn still replies.\n log.warn(\"DeepSeek stream returned empty content, falling back to chat drip\");\n return dripFromChat(request, emitter);\n }\n return aggregated.toString();\n }\n\n /** Blocking-path fallback (retries then local mock) dripped out as stream tokens. */\n private String dripFromChat(LlmRequest request, SseEmitter emitter) throws Exception {\n String full = chat(request);\n for (String token : full.split(\"\")) {\n emitter.send(SseEmitter.event().name(\"token\").data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n return full;\n }\n\n private List<Map<String, String>> buildMessages(LlmRequest request) {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", request.systemPromptOr(systemPrompt())));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n return messages;\n }\n\n private String doChat(LlmRequest request) throws Exception {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", request.systemPromptOr(systemPrompt())));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n Map<String, Object> body = Map.of(\n \"model\", model,\n \"messages\", messages,\n \"temperature\", request.temperature != null ? request.temperature : 0.72,\n \"max_tokens\", LlmClient.RESPONSE_MAX_TOKENS,\n \"stream\", false\n );\n\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .timeout(Duration.ofMillis(timeoutMs))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n throw new RuntimeException(\"DeepSeek API returned status \" + httpResponse.statusCode() + \": \" + httpResponse.body());\n }\n JsonNode root = objectMapper.readTree(httpResponse.body());\n JsonNode content = root.path(\"choices\").path(0).path(\"message\").path(\"content\");\n if (content.isMissingNode() || content.asText().isBlank()) {\n throw new RuntimeException(\"Unexpected DeepSeek response format: \" + httpResponse.body());\n }\n return content.asText();\n }\n\n private String systemPrompt() {\n return \"You are Aurora in Inner Cosmos. Be warm, specific, safe, and reflective. \"\n + \"Use the user's language. Ask at most one question. Never diagnose.\";\n }\n\n private String normalizeUrl(String value) {\n if (value == null || value.isBlank()) return \"https://api.deepseek.com/chat/completions\";\n if (value.endsWith(\"/chat/completions\")) return value;\n return value.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n\n private String escape(String token) {\n return token.replace(\"\\\\\", \"\\\\\\\\\")\n .replace(\"\\\"\", \"\\\\\\\"\")\n .replace(\"\\n\", \"\\\\n\")\n .replace(\"\\r\", \"\\\\r\")\n .replace(\"\\t\", \"\\\\t\");\n }\n}\n"}, "2702941cc15c1887c3a402c46da4a551b346e26e": {"t": 1785758378.7478359, "n": 10625, "h": "99274b551581794e", "c": "package com.innercosmos.ai.client;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.innercosmos.service.AiLogService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Executor;\n\npublic class DeepSeekLlmClient implements LlmClient {\n private static final Logger log = LoggerFactory.getLogger(DeepSeekLlmClient.class);\n private static final ObjectMapper objectMapper = new ObjectMapper();\n\n private final String apiKey;\n private final String baseUrl;\n private final String model;\n private final int timeoutMs;\n private final boolean allowFallback;\n private final AiLogService aiLogService;\n private final Executor aiExecutor;\n private final MockLlmClient fallback;\n private final HttpClient httpClient;\n\n public DeepSeekLlmClient(String apiKey, String baseUrl, String model, int timeoutMs,\n boolean allowFallback, AiLogService aiLogService, Executor aiExecutor) {\n this.apiKey = apiKey;\n this.baseUrl = normalizeUrl(baseUrl);\n this.model = model == null || model.isBlank() ? \"deepseek-chat\" : model;\n this.timeoutMs = timeoutMs <= 0 ? 30000 : timeoutMs;\n this.allowFallback = allowFallback;\n this.aiLogService = aiLogService;\n this.aiExecutor = aiExecutor;\n this.fallback = new MockLlmClient(aiExecutor);\n this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(20)).build();\n }\n\n @Override\n public String chat(LlmRequest request) {\n long start = System.currentTimeMillis();\n String response = null;\n boolean success = false;\n boolean fallbackUsed = false;\n String errorMessage = null;\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception firstError) {\n errorMessage = firstError.getMessage();\n log.warn(\"DeepSeek chat failed on first attempt: {}\", firstError.getMessage());\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw firstError;\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception retryError) {\n errorMessage = retryError.getMessage();\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed and fallback is disabled\", retryError);\n }\n log.error(\"DeepSeek chat failed on retry, falling back to local mock: {}\", retryError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n } finally {\n aiLogService.recordDetailed(request.userId, request.moduleName, fallbackUsed ? \"MOCK\" : \"DEEPSEEK\",\n fallbackUsed ? \"mock-inner-cosmos\" : model, request.prompt, response, request.requestJson,\n response, success, fallbackUsed, fallbackUsed ? errorMessage : (success ? null : errorMessage),\n System.currentTimeMillis() - start);\n }\n }\n\n @Override\n public SseEmitter streamChat(LlmRequest request) {\n SseEmitter emitter = new SseEmitter(120_000L);\n aiExecutor.execute(() -> {\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n String aggregated = streamRemote(request, emitter);\n if (aggregated == null) {\n return;\n }\n emitter.send(SseEmitter.event().name(\"done\").data(\"{\\\"message\\\":\\\"done\\\"}\"));\n emitter.complete();\n } catch (Exception exception) {\n emitter.completeWithError(exception);\n }\n });\n return emitter;\n }\n\n /**\n * Real provider SSE streaming (VS-003 §2). DeepSeek is OpenAI-compatible.\n */\n private String streamRemote(LlmRequest request, SseEmitter emitter) throws Exception {\n List<Map<String, String>> messages = buildMessages(request);\n Map<String, Object> body = Map.of(\n \"model\", model,\n \"messages\", messages,\n \"temperature\", request.temperature != null ? request.temperature : 0.72,\n \"max_tokens\", LlmClient.RESPONSE_MAX_TOKENS,\n \"stream\", true\n );\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"Accept\", \"text/event-stream\")\n .timeout(Duration.ofMillis(timeoutMs))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n try {\n java.util.stream.Stream<String> lines = httpClient.send(httpRequest,\n HttpResponse.BodyHandlers.ofLines()).body();\n StringBuilder aggregated = new StringBuilder();\n lines.forEach(line -> {\n if (line == null || !line.startsWith(\"data:\")) return;\n String payload = line.substring(5).trim();\n if (payload.isEmpty() || \"[DONE]\".equals(payload)) return;\n try {\n JsonNode delta = objectMapper.readTree(payload)\n .path(\"choices\").path(0).path(\"delta\").path(\"content\");\n if (!delta.isMissingNode() && !delta.asText().isEmpty()) {\n String token = delta.asText();\n aggregated.append(token);\n emitter.send(SseEmitter.event().name(\"token\")\n .data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n } catch (Exception parseEx) {\n log.debug(\"DeepSeek stream parse skip: {}\", parseEx.getMessage());\n }\n });\n return aggregated.toString();\n } catch (Exception remoteError) {\n log.warn(\"DeepSeek stream failed, falling back to chat drip: {}\", remoteError.getMessage());\n String full = chat(request);\n for (String token : full.split(\"\")) {\n emitter.send(SseEmitter.event().name(\"token\").data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n return full;\n }\n }\n\n private List<Map<String, String>> buildMessages(LlmRequest request) {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", request.systemPromptOr(systemPrompt())));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n return messages;\n }\n\n private String doChat(LlmRequest request) throws Exception {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", request.systemPromptOr(systemPrompt())));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n Map<String, Object> body = Map.of(\n \"model\", model,\n \"messages\", messages,\n \"temperature\", request.temperature != null ? request.temperature : 0.72,\n \"max_tokens\", LlmClient.RESPONSE_MAX_TOKENS,\n \"stream\", false\n );\n\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .timeout(Duration.ofMillis(timeoutMs))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n throw new RuntimeException(\"DeepSeek API returned status \" + httpResponse.statusCode() + \": \" + httpResponse.body());\n }\n JsonNode root = objectMapper.readTree(httpResponse.body());\n JsonNode content = root.path(\"choices\").path(0).path(\"message\").path(\"content\");\n if (content.isMissingNode() || content.asText().isBlank()) {\n throw new RuntimeException(\"Unexpected DeepSeek response format: \" + httpResponse.body());\n }\n return content.asText();\n }\n\n private String systemPrompt() {\n return \"You are Aurora in Inner Cosmos. Be warm, specific, safe, and reflective. \"\n + \"Use the user's language. Ask at most one question. Never diagnose.\";\n }\n\n private String normalizeUrl(String value) {\n if (value == null || value.isBlank()) return \"https://api.deepseek.com/chat/completions\";\n if (value.endsWith(\"/chat/completions\")) return value;\n return value.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n\n private String escape(String token) {\n return token.replace(\"\\\\\", \"\\\\\\\\\")\n .replace(\"\\\"\", \"\\\\\\\"\")\n .replace(\"\\n\", \"\\\\n\")\n .replace(\"\\r\", \"\\\\r\")\n .replace(\"\\t\", \"\\\\t\");\n }\n}\n"}, "72eed75c1cf10ff6cec3c7916fd267b21b445084": {"t": 1785758380.2460892, "n": 10577, "h": "ea4ec467e89881f1", "c": "package com.innercosmos.ai.client;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.innercosmos.service.AiLogService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Executor;\n\npublic class DeepSeekLlmClient implements LlmClient {\n private static final Logger log = LoggerFactory.getLogger(DeepSeekLlmClient.class);\n private static final ObjectMapper objectMapper = new ObjectMapper();\n\n private final String apiKey;\n private final String baseUrl;\n private final String model;\n private final int timeoutMs;\n private final boolean allowFallback;\n private final AiLogService aiLogService;\n private final Executor aiExecutor;\n private final MockLlmClient fallback;\n private final HttpClient httpClient;\n\n public DeepSeekLlmClient(String apiKey, String baseUrl, String model, int timeoutMs,\n boolean allowFallback, AiLogService aiLogService, Executor aiExecutor) {\n this.apiKey = apiKey;\n this.baseUrl = normalizeUrl(baseUrl);\n this.model = model == null || model.isBlank() ? \"deepseek-chat\" : model;\n this.timeoutMs = timeoutMs <= 0 ? 30000 : timeoutMs;\n this.allowFallback = allowFallback;\n this.aiLogService = aiLogService;\n this.aiExecutor = aiExecutor;\n this.fallback = new MockLlmClient(aiExecutor);\n this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(20)).build();\n }\n\n @Override\n public String chat(LlmRequest request) {\n long start = System.currentTimeMillis();\n String response = null;\n boolean success = false;\n boolean fallbackUsed = false;\n String errorMessage = null;\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception firstError) {\n errorMessage = firstError.getMessage();\n log.warn(\"DeepSeek chat failed on first attempt: {}\", firstError.getMessage());\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw firstError;\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception retryError) {\n errorMessage = retryError.getMessage();\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed and fallback is disabled\", retryError);\n }\n log.error(\"DeepSeek chat failed on retry, falling back to local mock: {}\", retryError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n } finally {\n aiLogService.recordDetailed(request.userId, request.moduleName, fallbackUsed ? \"MOCK\" : \"DEEPSEEK\",\n fallbackUsed ? \"mock-inner-cosmos\" : model, request.prompt, response, request.requestJson,\n response, success, fallbackUsed, fallbackUsed ? errorMessage : (success ? null : errorMessage),\n System.currentTimeMillis() - start);\n }\n }\n\n @Override\n public SseEmitter streamChat(LlmRequest request) {\n SseEmitter emitter = new SseEmitter(120_000L);\n aiExecutor.execute(() -> {\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n String aggregated = streamRemote(request, emitter);\n if (aggregated == null) {\n return;\n }\n emitter.send(SseEmitter.event().name(\"done\").data(\"{\\\"message\\\":\\\"done\\\"}\"));\n emitter.complete();\n } catch (Exception exception) {\n emitter.completeWithError(exception);\n }\n });\n return emitter;\n }\n\n /**\n * Real provider SSE streaming (VS-003 §2). DeepSeek is OpenAI-compatible.\n */\n private String streamRemote(LlmRequest request, SseEmitter emitter) throws Exception {\n List<Map<String, String>> messages = buildMessages(request);\n Map<String, Object> body = Map.of(\n \"model\", model,\n \"messages\", messages,\n \"temperature\", request.temperature != null ? request.temperature : 0.72,\n \"max_tokens\", LlmClient.RESPONSE_MAX_TOKENS,\n \"stream\", true\n );\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"Accept\", \"text/event-stream\")\n .timeout(Duration.ofMillis(timeoutMs))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n try {\n java.util.stream.Stream<String> lines = httpClient.send(httpRequest,\n HttpResponse.BodyHandlers.ofLines()).body();\n StringBuilder aggregated = new StringBuilder();\n lines.forEach(line -> {\n if (line == null || !line.startsWith(\"data:\")) return;\n String payload = line.substring(5).trim();\n if (payload.isEmpty() || \"[DONE]\".equals(payload)) return;\n try {\n JsonNode delta = objectMapper.readTree(payload)\n .path(\"choices\").path(0).path(\"delta\").path(\"content\");\n if (!delta.isMissingNode() && !delta.asText().isEmpty()) {\n String token = delta.asText();\n aggregated.append(token);\n emitter.send(SseEmitter.event().name(\"token\")\n .data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n } catch (Exception parseEx) {\n log.debug(\"DeepSeek stream parse skip: {}\", parseEx.getMessage());\n }\n });\n return aggregated.toString();\n } catch (Exception remoteError) {\n log.warn(\"DeepSeek stream failed, falling back to chat drip: {}\", remoteError.getMessage());\n String full = chat(request);\n for (String token : full.split(\"\")) {\n emitter.send(SseEmitter.event().name(\"token\").data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n return full;\n }\n }\n\n private List<Map<String, String>> buildMessages(LlmRequest request) {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", systemPrompt()));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n return messages;\n }\n\n private String doChat(LlmRequest request) throws Exception {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", systemPrompt()));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n Map<String, Object> body = Map.of(\n \"model\", model,\n \"messages\", messages,\n \"temperature\", request.temperature != null ? request.temperature : 0.72,\n \"max_tokens\", LlmClient.RESPONSE_MAX_TOKENS,\n \"stream\", false\n );\n\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .timeout(Duration.ofMillis(timeoutMs))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n throw new RuntimeException(\"DeepSeek API returned status \" + httpResponse.statusCode() + \": \" + httpResponse.body());\n }\n JsonNode root = objectMapper.readTree(httpResponse.body());\n JsonNode content = root.path(\"choices\").path(0).path(\"message\").path(\"content\");\n if (content.isMissingNode() || content.asText().isBlank()) {\n throw new RuntimeException(\"Unexpected DeepSeek response format: \" + httpResponse.body());\n }\n return content.asText();\n }\n\n private String systemPrompt() {\n return \"You are Aurora in Inner Cosmos. Be warm, specific, safe, and reflective. \"\n + \"Use the user's language. Ask at most one question. Never diagnose.\";\n }\n\n private String normalizeUrl(String value) {\n if (value == null || value.isBlank()) return \"https://api.deepseek.com/chat/completions\";\n if (value.endsWith(\"/chat/completions\")) return value;\n return value.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n\n private String escape(String token) {\n return token.replace(\"\\\\\", \"\\\\\\\\\")\n .replace(\"\\\"\", \"\\\\\\\"\")\n .replace(\"\\n\", \"\\\\n\")\n .replace(\"\\r\", \"\\\\r\")\n .replace(\"\\t\", \"\\\\t\");\n }\n}\n"}, "28dbf0085b4422c46d8c347c27795f934d3291d4": {"t": 1785758381.669076, "n": 10473, "h": "de3611f5d48cfce2", "c": "package com.innercosmos.ai.client;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.innercosmos.service.AiLogService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Executor;\n\npublic class DeepSeekLlmClient implements LlmClient {\n private static final Logger log = LoggerFactory.getLogger(DeepSeekLlmClient.class);\n private static final ObjectMapper objectMapper = new ObjectMapper();\n\n private final String apiKey;\n private final String baseUrl;\n private final String model;\n private final int timeoutMs;\n private final boolean allowFallback;\n private final AiLogService aiLogService;\n private final Executor aiExecutor;\n private final MockLlmClient fallback;\n private final HttpClient httpClient;\n\n public DeepSeekLlmClient(String apiKey, String baseUrl, String model, int timeoutMs,\n boolean allowFallback, AiLogService aiLogService, Executor aiExecutor) {\n this.apiKey = apiKey;\n this.baseUrl = normalizeUrl(baseUrl);\n this.model = model == null || model.isBlank() ? \"deepseek-chat\" : model;\n this.timeoutMs = timeoutMs <= 0 ? 30000 : timeoutMs;\n this.allowFallback = allowFallback;\n this.aiLogService = aiLogService;\n this.aiExecutor = aiExecutor;\n this.fallback = new MockLlmClient(aiExecutor);\n this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(20)).build();\n }\n\n @Override\n public String chat(LlmRequest request) {\n long start = System.currentTimeMillis();\n String response = null;\n boolean success = false;\n boolean fallbackUsed = false;\n String errorMessage = null;\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception firstError) {\n errorMessage = firstError.getMessage();\n log.warn(\"DeepSeek chat failed on first attempt: {}\", firstError.getMessage());\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw firstError;\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception retryError) {\n errorMessage = retryError.getMessage();\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed and fallback is disabled\", retryError);\n }\n log.error(\"DeepSeek chat failed on retry, falling back to local mock: {}\", retryError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n } finally {\n aiLogService.recordDetailed(request.userId, request.moduleName, fallbackUsed ? \"MOCK\" : \"DEEPSEEK\",\n fallbackUsed ? \"mock-inner-cosmos\" : model, request.prompt, response, request.requestJson,\n response, success, fallbackUsed, fallbackUsed ? errorMessage : (success ? null : errorMessage),\n System.currentTimeMillis() - start);\n }\n }\n\n @Override\n public SseEmitter streamChat(LlmRequest request) {\n SseEmitter emitter = new SseEmitter(120_000L);\n aiExecutor.execute(() -> {\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n String aggregated = streamRemote(request, emitter);\n if (aggregated == null) {\n return;\n }\n emitter.send(SseEmitter.event().name(\"done\").data(\"{\\\"message\\\":\\\"done\\\"}\"));\n emitter.complete();\n } catch (Exception exception) {\n emitter.completeWithError(exception);\n }\n });\n return emitter;\n }\n\n /**\n * Real provider SSE streaming (VS-003 §2). DeepSeek is OpenAI-compatible.\n */\n private String streamRemote(LlmRequest request, SseEmitter emitter) throws Exception {\n List<Map<String, String>> messages = buildMessages(request);\n Map<String, Object> body = Map.of(\n \"model\", model,\n \"messages\", messages,\n \"temperature\", 0.72,\n \"max_tokens\", LlmClient.RESPONSE_MAX_TOKENS,\n \"stream\", true\n );\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"Accept\", \"text/event-stream\")\n .timeout(Duration.ofMillis(timeoutMs))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n try {\n java.util.stream.Stream<String> lines = httpClient.send(httpRequest,\n HttpResponse.BodyHandlers.ofLines()).body();\n StringBuilder aggregated = new StringBuilder();\n lines.forEach(line -> {\n if (line == null || !line.startsWith(\"data:\")) return;\n String payload = line.substring(5).trim();\n if (payload.isEmpty() || \"[DONE]\".equals(payload)) return;\n try {\n JsonNode delta = objectMapper.readTree(payload)\n .path(\"choices\").path(0).path(\"delta\").path(\"content\");\n if (!delta.isMissingNode() && !delta.asText().isEmpty()) {\n String token = delta.asText();\n aggregated.append(token);\n emitter.send(SseEmitter.event().name(\"token\")\n .data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n } catch (Exception parseEx) {\n log.debug(\"DeepSeek stream parse skip: {}\", parseEx.getMessage());\n }\n });\n return aggregated.toString();\n } catch (Exception remoteError) {\n log.warn(\"DeepSeek stream failed, falling back to chat drip: {}\", remoteError.getMessage());\n String full = chat(request);\n for (String token : full.split(\"\")) {\n emitter.send(SseEmitter.event().name(\"token\").data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n return full;\n }\n }\n\n private List<Map<String, String>> buildMessages(LlmRequest request) {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", systemPrompt()));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n return messages;\n }\n\n private String doChat(LlmRequest request) throws Exception {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", systemPrompt()));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n Map<String, Object> body = Map.of(\n \"model\", model,\n \"messages\", messages,\n \"temperature\", 0.72,\n \"max_tokens\", LlmClient.RESPONSE_MAX_TOKENS,\n \"stream\", false\n );\n\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .timeout(Duration.ofMillis(timeoutMs))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n throw new RuntimeException(\"DeepSeek API returned status \" + httpResponse.statusCode() + \": \" + httpResponse.body());\n }\n JsonNode root = objectMapper.readTree(httpResponse.body());\n JsonNode content = root.path(\"choices\").path(0).path(\"message\").path(\"content\");\n if (content.isMissingNode() || content.asText().isBlank()) {\n throw new RuntimeException(\"Unexpected DeepSeek response format: \" + httpResponse.body());\n }\n return content.asText();\n }\n\n private String systemPrompt() {\n return \"You are Aurora in Inner Cosmos. Be warm, specific, safe, and reflective. \"\n + \"Use the user's language. Ask at most one question. Never diagnose.\";\n }\n\n private String normalizeUrl(String value) {\n if (value == null || value.isBlank()) return \"https://api.deepseek.com/chat/completions\";\n if (value.endsWith(\"/chat/completions\")) return value;\n return value.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n\n private String escape(String token) {\n return token.replace(\"\\\\\", \"\\\\\\\\\")\n .replace(\"\\\"\", \"\\\\\\\"\")\n .replace(\"\\n\", \"\\\\n\")\n .replace(\"\\r\", \"\\\\r\")\n .replace(\"\\t\", \"\\\\t\");\n }\n}\n"}, "eb19fb9b0e411f989a5e706fccea85f8d6a9ac80": {"t": 1785758382.8949568, "n": 10421, "h": "f888765fa8bb2917", "c": "package com.innercosmos.ai.client;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.innercosmos.service.AiLogService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Executor;\n\npublic class DeepSeekLlmClient implements LlmClient {\n private static final Logger log = LoggerFactory.getLogger(DeepSeekLlmClient.class);\n private static final ObjectMapper objectMapper = new ObjectMapper();\n\n private final String apiKey;\n private final String baseUrl;\n private final String model;\n private final int timeoutMs;\n private final boolean allowFallback;\n private final AiLogService aiLogService;\n private final Executor aiExecutor;\n private final MockLlmClient fallback;\n private final HttpClient httpClient;\n\n public DeepSeekLlmClient(String apiKey, String baseUrl, String model, int timeoutMs,\n boolean allowFallback, AiLogService aiLogService, Executor aiExecutor) {\n this.apiKey = apiKey;\n this.baseUrl = normalizeUrl(baseUrl);\n this.model = model == null || model.isBlank() ? \"deepseek-chat\" : model;\n this.timeoutMs = timeoutMs <= 0 ? 30000 : timeoutMs;\n this.allowFallback = allowFallback;\n this.aiLogService = aiLogService;\n this.aiExecutor = aiExecutor;\n this.fallback = new MockLlmClient(aiExecutor);\n this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(20)).build();\n }\n\n @Override\n public String chat(LlmRequest request) {\n long start = System.currentTimeMillis();\n String response = null;\n boolean success = false;\n boolean fallbackUsed = false;\n String errorMessage = null;\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception firstError) {\n errorMessage = firstError.getMessage();\n log.warn(\"DeepSeek chat failed on first attempt: {}\", firstError.getMessage());\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw firstError;\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception retryError) {\n errorMessage = retryError.getMessage();\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed and fallback is disabled\", retryError);\n }\n log.error(\"DeepSeek chat failed on retry, falling back to local mock: {}\", retryError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n } finally {\n aiLogService.recordDetailed(request.userId, request.moduleName, fallbackUsed ? \"MOCK\" : \"DEEPSEEK\",\n fallbackUsed ? \"mock-inner-cosmos\" : model, request.prompt, response, request.requestJson,\n response, success, fallbackUsed, fallbackUsed ? errorMessage : (success ? null : errorMessage),\n System.currentTimeMillis() - start);\n }\n }\n\n @Override\n public SseEmitter streamChat(LlmRequest request) {\n SseEmitter emitter = new SseEmitter(120_000L);\n aiExecutor.execute(() -> {\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n String aggregated = streamRemote(request, emitter);\n if (aggregated == null) {\n return;\n }\n emitter.send(SseEmitter.event().name(\"done\").data(\"{\\\"message\\\":\\\"done\\\"}\"));\n emitter.complete();\n } catch (Exception exception) {\n emitter.completeWithError(exception);\n }\n });\n return emitter;\n }\n\n /**\n * Real provider SSE streaming (VS-003 §2). DeepSeek is OpenAI-compatible.\n */\n private String streamRemote(LlmRequest request, SseEmitter emitter) throws Exception {\n List<Map<String, String>> messages = buildMessages(request);\n Map<String, Object> body = Map.of(\n \"model\", model,\n \"messages\", messages,\n \"temperature\", 0.72,\n \"max_tokens\", 900,\n \"stream\", true\n );\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"Accept\", \"text/event-stream\")\n .timeout(Duration.ofMillis(timeoutMs))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n try {\n java.util.stream.Stream<String> lines = httpClient.send(httpRequest,\n HttpResponse.BodyHandlers.ofLines()).body();\n StringBuilder aggregated = new StringBuilder();\n lines.forEach(line -> {\n if (line == null || !line.startsWith(\"data:\")) return;\n String payload = line.substring(5).trim();\n if (payload.isEmpty() || \"[DONE]\".equals(payload)) return;\n try {\n JsonNode delta = objectMapper.readTree(payload)\n .path(\"choices\").path(0).path(\"delta\").path(\"content\");\n if (!delta.isMissingNode() && !delta.asText().isEmpty()) {\n String token = delta.asText();\n aggregated.append(token);\n emitter.send(SseEmitter.event().name(\"token\")\n .data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n } catch (Exception parseEx) {\n log.debug(\"DeepSeek stream parse skip: {}\", parseEx.getMessage());\n }\n });\n return aggregated.toString();\n } catch (Exception remoteError) {\n log.warn(\"DeepSeek stream failed, falling back to chat drip: {}\", remoteError.getMessage());\n String full = chat(request);\n for (String token : full.split(\"\")) {\n emitter.send(SseEmitter.event().name(\"token\").data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n }\n return full;\n }\n }\n\n private List<Map<String, String>> buildMessages(LlmRequest request) {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", systemPrompt()));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n return messages;\n }\n\n private String doChat(LlmRequest request) throws Exception {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", systemPrompt()));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n Map<String, Object> body = Map.of(\n \"model\", model,\n \"messages\", messages,\n \"temperature\", 0.72,\n \"max_tokens\", 900,\n \"stream\", false\n );\n\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .timeout(Duration.ofMillis(timeoutMs))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n throw new RuntimeException(\"DeepSeek API returned status \" + httpResponse.statusCode() + \": \" + httpResponse.body());\n }\n JsonNode root = objectMapper.readTree(httpResponse.body());\n JsonNode content = root.path(\"choices\").path(0).path(\"message\").path(\"content\");\n if (content.isMissingNode() || content.asText().isBlank()) {\n throw new RuntimeException(\"Unexpected DeepSeek response format: \" + httpResponse.body());\n }\n return content.asText();\n }\n\n private String systemPrompt() {\n return \"You are Aurora in Inner Cosmos. Be warm, specific, safe, and reflective. \"\n + \"Use the user's language. Ask at most one question. Never diagnose.\";\n }\n\n private String normalizeUrl(String value) {\n if (value == null || value.isBlank()) return \"https://api.deepseek.com/chat/completions\";\n if (value.endsWith(\"/chat/completions\")) return value;\n return value.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n\n private String escape(String token) {\n return token.replace(\"\\\\\", \"\\\\\\\\\")\n .replace(\"\\\"\", \"\\\\\\\"\")\n .replace(\"\\n\", \"\\\\n\")\n .replace(\"\\r\", \"\\\\r\")\n .replace(\"\\t\", \"\\\\t\");\n }\n}\n"}, "8459d5304acad56acc7de3031d1fc4dfa3745ca7": {"t": 1785758384.255275, "n": 7188, "h": "3d9781f4d2beff75", "c": "package com.innercosmos.ai.client;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.innercosmos.service.AiLogService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Executor;\n\npublic class DeepSeekLlmClient implements LlmClient {\n private static final Logger log = LoggerFactory.getLogger(DeepSeekLlmClient.class);\n private static final ObjectMapper objectMapper = new ObjectMapper();\n\n private final String apiKey;\n private final String baseUrl;\n private final String model;\n private final int timeoutMs;\n private final boolean allowFallback;\n private final AiLogService aiLogService;\n private final Executor aiExecutor;\n private final MockLlmClient fallback;\n private final HttpClient httpClient;\n\n public DeepSeekLlmClient(String apiKey, String baseUrl, String model, int timeoutMs,\n boolean allowFallback, AiLogService aiLogService, Executor aiExecutor) {\n this.apiKey = apiKey;\n this.baseUrl = normalizeUrl(baseUrl);\n this.model = model == null || model.isBlank() ? \"deepseek-chat\" : model;\n this.timeoutMs = timeoutMs <= 0 ? 30000 : timeoutMs;\n this.allowFallback = allowFallback;\n this.aiLogService = aiLogService;\n this.aiExecutor = aiExecutor;\n this.fallback = new MockLlmClient(aiExecutor);\n this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(20)).build();\n }\n\n @Override\n public String chat(LlmRequest request) {\n long start = System.currentTimeMillis();\n String response = null;\n boolean success = false;\n boolean fallbackUsed = false;\n String errorMessage = null;\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is empty\");\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception firstError) {\n errorMessage = firstError.getMessage();\n log.warn(\"DeepSeek chat failed on first attempt: {}\", firstError.getMessage());\n try {\n if (apiKey == null || apiKey.isBlank()) {\n throw firstError;\n }\n response = doChat(request);\n success = true;\n return response;\n } catch (Exception retryError) {\n errorMessage = retryError.getMessage();\n if (!allowFallback) {\n throw new RuntimeException(\"DeepSeek remote chat failed and fallback is disabled\", retryError);\n }\n log.error(\"DeepSeek chat failed on retry, falling back to local mock: {}\", retryError.getMessage());\n response = fallback.chat(request);\n fallbackUsed = true;\n success = true;\n return response;\n }\n } finally {\n aiLogService.recordDetailed(request.userId, request.moduleName, fallbackUsed ? \"MOCK\" : \"DEEPSEEK\",\n fallbackUsed ? \"mock-inner-cosmos\" : model, request.prompt, response, request.requestJson,\n response, success, fallbackUsed, fallbackUsed ? errorMessage : (success ? null : errorMessage),\n System.currentTimeMillis() - start);\n }\n }\n\n @Override\n public SseEmitter streamChat(LlmRequest request) {\n SseEmitter emitter = new SseEmitter(60_000L);\n aiExecutor.execute(() -> {\n try {\n String response = chat(request);\n for (String token : response.split(\"\")) {\n emitter.send(SseEmitter.event().name(\"token\").data(\"{\\\"content\\\":\\\"\" + escape(token) + \"\\\"}\"));\n Thread.sleep(16);\n }\n emitter.send(SseEmitter.event().name(\"done\").data(\"{\\\"message\\\":\\\"done\\\"}\"));\n emitter.complete();\n } catch (Exception exception) {\n emitter.completeWithError(exception);\n }\n });\n return emitter;\n }\n\n private String doChat(LlmRequest request) throws Exception {\n List<Map<String, String>> messages = new ArrayList<>();\n messages.add(Map.of(\"role\", \"system\", \"content\", systemPrompt()));\n if (request.recentMessages != null) {\n for (String recent : request.recentMessages) {\n if (recent != null && !recent.isBlank()) {\n messages.add(Map.of(\"role\", \"user\", \"content\", \"Context note: \" + recent));\n }\n }\n }\n messages.add(Map.of(\"role\", \"user\", \"content\", request.prompt == null ? \"\" : request.prompt));\n Map<String, Object> body = Map.of(\n \"model\", model,\n \"messages\", messages,\n \"temperature\", 0.72,\n \"max_tokens\", 900,\n \"stream\", false\n );\n\n HttpRequest httpRequest = HttpRequest.newBuilder()\n .uri(URI.create(baseUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .timeout(Duration.ofMillis(timeoutMs))\n .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))\n .build();\n HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());\n if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {\n throw new RuntimeException(\"DeepSeek API returned status \" + httpResponse.statusCode() + \": \" + httpResponse.body());\n }\n JsonNode root = objectMapper.readTree(httpResponse.body());\n JsonNode content = root.path(\"choices\").path(0).path(\"message\").path(\"content\");\n if (content.isMissingNode() || content.asText().isBlank()) {\n throw new RuntimeException(\"Unexpected DeepSeek response format: \" + httpResponse.body());\n }\n return content.asText();\n }\n\n private String systemPrompt() {\n return \"You are Aurora in Inner Cosmos. Be warm, specific, safe, and reflective. \"\n + \"Use the user's language. Ask at most one question. Never diagnose.\";\n }\n\n private String normalizeUrl(String value) {\n if (value == null || value.isBlank()) return \"https://api.deepseek.com/chat/completions\";\n if (value.endsWith(\"/chat/completions\")) return value;\n return value.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n\n private String escape(String token) {\n return token.replace(\"\\\\\", \"\\\\\\\\\")\n .replace(\"\\\"\", \"\\\\\\\"\")\n .replace(\"\\n\", \"\\\\n\")\n .replace(\"\\r\", \"\\\\r\")\n .replace(\"\\t\", \"\\\\t\");\n }\n}\n"}, "cbf4645290d5bf4718ab5a5fb971eb03ec0a0940": {"t": 1785758385.6672666, "n": 564, "h": "8e7d745cd87ea41d", "c": "package com.innercosmos.ai.client;\n\nimport com.innercosmos.exception.AiProviderException;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\npublic class DeepSeekLlmClient implements LlmClient {\n @Override\n public String chat(LlmRequest request) {\n throw new AiProviderException(\"DeepSeek remote mode is reserved for later configuration.\");\n }\n\n @Override\n public SseEmitter streamChat(LlmRequest request) {\n throw new AiProviderException(\"DeepSeek streaming mode is reserved for later configuration.\");\n }\n}\n"}}}}