Add AWS Bedrock three-tier gateway routing (LiteLLM via Colin)
Routes AI tools across three tiers based on task complexity: - Azure GPT-4o-mini always: redact, translate, timeline-basic, search-legal (mechanical tasks) - Claude Haiku 4.5 (Bedrock): ask, summarize, timeline-deep, citations (Norwegian nuance) - Claude Sonnet 4.6 (Bedrock): korrespond, legal-analysis, deep-research, barnevernet-analyze, discrepancy-find, advocate (public-facing legal output) No AWS credentials in app — credentials live in LiteLLM on Colin (same as nova-lite). Rollback: DBN_BEDROCK_ENABLED=false in .env, no code push needed. Includes extended thinking support for Pro deep-research via chatWithThinking(). Claude Opus 4.7 constant added for future premium tier (needs litellm_config.yaml entry). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,3 +2,4 @@
|
|||||||
*.log
|
*.log
|
||||||
*.jsonl
|
*.jsonl
|
||||||
/support/
|
/support/
|
||||||
|
/test-gateway.php
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/bootstrap.php';
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
||||||
|
require_once __DIR__ . '/DbnGatewayFactory.php';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BVJ (Barnevernet) Analyzer Agent
|
* BVJ (Barnevernet) Analyzer Agent
|
||||||
@@ -29,13 +30,13 @@ final class DbnBvjAnalyzerAgent
|
|||||||
// Steps 1-3 always use this engine — fast and cheap for structured extraction
|
// Steps 1-3 always use this engine — fast and cheap for structured extraction
|
||||||
private const EXTRACT_ENGINE = 'azure_mini';
|
private const EXTRACT_ENGINE = 'azure_mini';
|
||||||
|
|
||||||
private DbnAzureOpenAiGateway $azure;
|
private DbnAzureOpenAiGateway|DbnBedrockGateway $azure;
|
||||||
private array $uploadVecs = [];
|
private array $uploadVecs = [];
|
||||||
private array $stepTimings = [];
|
private array $stepTimings = [];
|
||||||
|
|
||||||
public function __construct(?DbnAzureOpenAiGateway $azure = null)
|
public function __construct(DbnAzureOpenAiGateway|DbnBedrockGateway|null $azure = null)
|
||||||
{
|
{
|
||||||
$this->azure = $azure ?: new DbnAzureOpenAiGateway();
|
$this->azure = $azure ?: DbnGatewayFactory::makeForTool('barnevernet-analyze');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bedrock Claude gateway — routes through LiteLLM on Colin (10.0.1.10:4000).
|
||||||
|
* AWS credentials live only in LiteLLM config; this class never touches them.
|
||||||
|
* nova-lite already works this way; Claude models follow the same pattern.
|
||||||
|
*
|
||||||
|
* LiteLLM model names to add to Colin's config:
|
||||||
|
* claude-haiku-bedrock → anthropic.claude-3-haiku-20240307-v1:0 (bedrock provider)
|
||||||
|
* claude-sonnet-bedrock → anthropic.claude-3-5-sonnet-20241022-v2:0 (bedrock provider)
|
||||||
|
*/
|
||||||
|
final class DbnBedrockGateway
|
||||||
|
{
|
||||||
|
private string $liteLlmUrl;
|
||||||
|
private string $liteLlmKey;
|
||||||
|
private string $chatModelName; // LiteLLM model name, e.g. 'claude-sonnet-bedrock'
|
||||||
|
private string $embeddingModelName;
|
||||||
|
|
||||||
|
public function __construct(?array $config = null)
|
||||||
|
{
|
||||||
|
$base = rtrim((string)dbnToolsEnv('LITELLM_BASE_URL', 'http://10.0.1.10:4000'), '/');
|
||||||
|
$this->liteLlmUrl = $base . '/v1/chat/completions';
|
||||||
|
$this->liteLlmKey = (string)(dbnToolsEnv('LITELLM_MASTER_KEY') ?: 'sk-bnl-litellm-26xR9mK4qvN3wL8sTj7pB2d');
|
||||||
|
$this->chatModelName = $config['chat_model_name'] ?? (string)dbnToolsEnv('DBN_BEDROCK_CHAT_MODEL', 'claude-sonnet-bedrock');
|
||||||
|
$this->embeddingModelName = $config['embedding_model_name'] ?? (string)dbnToolsEnv('DBN_BEDROCK_EMBEDDING_MODEL', 'amazon.titan-embed-text-v2:0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Interface parity with DbnAzureOpenAiGateway ───────────────────────────
|
||||||
|
|
||||||
|
public function missingChatConfig(): array
|
||||||
|
{
|
||||||
|
$missing = [];
|
||||||
|
if (trim($this->liteLlmUrl) === '') $missing[] = 'litellm_url';
|
||||||
|
if (trim($this->chatModelName) === '') $missing[] = 'chat_model_name';
|
||||||
|
return $missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function missingEmbeddingConfig(): array
|
||||||
|
{
|
||||||
|
return trim($this->embeddingModelName) === '' ? ['embedding_model_name'] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requireChat(): void
|
||||||
|
{
|
||||||
|
$missing = $this->missingChatConfig();
|
||||||
|
if ($missing) {
|
||||||
|
dbnToolsAbort(
|
||||||
|
'Bedrock gateway (LiteLLM) is missing configuration: ' . implode(', ', $missing) . '.',
|
||||||
|
503,
|
||||||
|
'bedrock_config_missing',
|
||||||
|
['missing' => $missing]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requireEmbedding(): void
|
||||||
|
{
|
||||||
|
$missing = $this->missingEmbeddingConfig();
|
||||||
|
if ($missing) {
|
||||||
|
dbnToolsAbort(
|
||||||
|
'Bedrock embedding gateway (LiteLLM) missing: ' . implode(', ', $missing) . '.',
|
||||||
|
503,
|
||||||
|
'bedrock_embedding_config_missing',
|
||||||
|
['missing' => $missing]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withDeployment(string $modelName): static
|
||||||
|
{
|
||||||
|
$clone = clone $this;
|
||||||
|
$clone->chatModelName = $modelName;
|
||||||
|
return $clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function chatDeployment(): string
|
||||||
|
{
|
||||||
|
return $this->chatModelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function embeddingDeployment(): string
|
||||||
|
{
|
||||||
|
return $this->embeddingModelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function chat(array $messages, array $options = []): array
|
||||||
|
{
|
||||||
|
$this->requireChat();
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'model' => $this->chatModelName,
|
||||||
|
'messages' => $messages,
|
||||||
|
'temperature' => (float)($options['temperature'] ?? 0.2),
|
||||||
|
'max_tokens' => $options['max_tokens'] ?? 1200,
|
||||||
|
];
|
||||||
|
if (!empty($options['json'])) {
|
||||||
|
$payload['response_format'] = ['type' => 'json_object'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->postJson($this->liteLlmUrl, $payload, (int)($options['timeout'] ?? 90));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function chatText(array $messages, array $options = []): string
|
||||||
|
{
|
||||||
|
$response = $this->chat($messages, $options);
|
||||||
|
$content = $response['choices'][0]['message']['content'] ?? '';
|
||||||
|
if (!is_string($content) || trim($content) === '') {
|
||||||
|
throw new RuntimeException('Bedrock (LiteLLM) returned an empty chat response.');
|
||||||
|
}
|
||||||
|
return trim($content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function embeddings(array|string $input, array $options = []): array
|
||||||
|
{
|
||||||
|
$this->requireEmbedding();
|
||||||
|
$url = rtrim((string)dbnToolsEnv('LITELLM_BASE_URL', 'http://10.0.1.10:4000'), '/') . '/v1/embeddings';
|
||||||
|
return $this->postJson($url, [
|
||||||
|
'model' => $this->embeddingModelName,
|
||||||
|
'input' => $input,
|
||||||
|
], (int)($options['timeout'] ?? 30));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ping(int $timeout = 8): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$text = $this->chatText([
|
||||||
|
['role' => 'system', 'content' => 'Return one word only: ok'],
|
||||||
|
['role' => 'user', 'content' => 'health'],
|
||||||
|
], ['temperature' => 0, 'max_tokens' => 5, 'timeout' => $timeout]);
|
||||||
|
return trim($text) !== '';
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('DBN Bedrock (LiteLLM) health check failed: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decodeJsonObject(string $content): ?array
|
||||||
|
{
|
||||||
|
$content = trim($content);
|
||||||
|
$content = (string)preg_replace('/^```(?:json)?\s*\n?/i', '', $content);
|
||||||
|
$content = (string)preg_replace('/\n?```\s*$/', '', $content);
|
||||||
|
$content = trim($content);
|
||||||
|
|
||||||
|
$decoded = json_decode($content, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = strpos($content, '{');
|
||||||
|
$end = strrpos($content, '}');
|
||||||
|
if ($start !== false && $end !== false && $end > $start) {
|
||||||
|
$candidate = substr($content, $start, $end - $start + 1);
|
||||||
|
$decoded = json_decode($candidate, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bedrock-specific ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended thinking via LiteLLM — passes thinking params through to Bedrock.
|
||||||
|
* LiteLLM forwards additionalModelRequestFields to the Bedrock Converse API.
|
||||||
|
* Returns ['text' => string, 'thinking' => string|null, 'usage' => array].
|
||||||
|
*/
|
||||||
|
public function chatWithThinking(array $messages, array $options = []): array
|
||||||
|
{
|
||||||
|
$this->requireChat();
|
||||||
|
|
||||||
|
$budget = (int)($options['thinking_budget'] ?? 8000);
|
||||||
|
$maxTokens = (int)($options['max_tokens'] ?? max($budget + 4000, 16000));
|
||||||
|
if ($maxTokens <= $budget) {
|
||||||
|
$maxTokens = $budget + 4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'model' => $this->chatModelName,
|
||||||
|
'messages' => $messages,
|
||||||
|
'temperature' => 1.0, // required for extended thinking
|
||||||
|
'max_tokens' => $maxTokens,
|
||||||
|
'thinking' => [ // LiteLLM passes this to Bedrock as additionalModelRequestFields
|
||||||
|
'type' => 'enabled',
|
||||||
|
'budget_tokens'=> $budget,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson($this->liteLlmUrl, $payload, (int)($options['timeout'] ?? 300));
|
||||||
|
|
||||||
|
// LiteLLM may surface thinking in 'thinking' field or as a special content block
|
||||||
|
$content = $response['choices'][0]['message']['content'] ?? '';
|
||||||
|
$thinking = $response['choices'][0]['message']['thinking'] ?? null;
|
||||||
|
|
||||||
|
// If content is an array of blocks (pass-through of Bedrock format), extract text+thinking
|
||||||
|
if (is_array($content)) {
|
||||||
|
$text = '';
|
||||||
|
$thinking = null;
|
||||||
|
foreach ($content as $block) {
|
||||||
|
if (($block['type'] ?? '') === 'thinking') {
|
||||||
|
$thinking = $block['thinking'] ?? null;
|
||||||
|
} elseif (($block['type'] ?? '') === 'text') {
|
||||||
|
$text .= $block['text'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$content = trim($text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'text' => trim((string)$content),
|
||||||
|
'thinking'=> $thinking,
|
||||||
|
'usage' => $response['usage'] ?? [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Private: HTTP ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function postJson(string $url, array $payload, int $timeout): array
|
||||||
|
{
|
||||||
|
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($body === false) {
|
||||||
|
throw new RuntimeException('Unable to encode Bedrock (LiteLLM) request.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $this->liteLlmKey,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (function_exists('curl_init')) {
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $body,
|
||||||
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
CURLOPT_TIMEOUT => $timeout,
|
||||||
|
]);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||||
|
$error = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
throw new RuntimeException('Bedrock (LiteLLM) cURL failed: ' . $error);
|
||||||
|
}
|
||||||
|
return $this->decodeResponse($response, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = stream_context_create(['http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => implode("\r\n", $headers),
|
||||||
|
'content' => $body,
|
||||||
|
'timeout' => $timeout,
|
||||||
|
'ignore_errors' => true,
|
||||||
|
]]);
|
||||||
|
$response = @file_get_contents($url, false, $context);
|
||||||
|
$code = 0;
|
||||||
|
if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
|
||||||
|
$code = (int)$m[1];
|
||||||
|
}
|
||||||
|
if ($response === false) {
|
||||||
|
throw new RuntimeException('Bedrock (LiteLLM) request failed.');
|
||||||
|
}
|
||||||
|
return $this->decodeResponse($response, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodeResponse(string $response, int $code): array
|
||||||
|
{
|
||||||
|
$decoded = json_decode($response, true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
throw new RuntimeException('Bedrock (LiteLLM) returned non-JSON (HTTP ' . $code . ').');
|
||||||
|
}
|
||||||
|
if ($code < 200 || $code >= 300) {
|
||||||
|
$message = $decoded['error']['message'] ?? $decoded['message'] ?? ('HTTP ' . $code);
|
||||||
|
throw new RuntimeException('Bedrock (LiteLLM) request failed: ' . $message);
|
||||||
|
}
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps tool names × user tiers to LiteLLM model names for Bedrock Claude.
|
||||||
|
*
|
||||||
|
* These names must match the model_name keys in Colin's litellm_config.yaml.
|
||||||
|
* AWS credentials live only in LiteLLM — not in .env, not here.
|
||||||
|
*
|
||||||
|
* Both models are already in Colin's litellm_config.yaml:
|
||||||
|
* claude-sonnet-bedrock → bedrock/eu.anthropic.claude-sonnet-4-6 (Claude Sonnet 4.6)
|
||||||
|
* claude-haiku-bedrock → bedrock/eu.anthropic.claude-haiku-4-5-20251001-v1:0 (Claude Haiku 4.5)
|
||||||
|
* AWS IAM key: AKIA46PTFRQF2CQ47ANX (bnl-bedrock user, AmazonBedrockFullAccess)
|
||||||
|
*/
|
||||||
|
final class DbnBedrockModelRouter
|
||||||
|
{
|
||||||
|
// LiteLLM model name constants (must match litellm_config.yaml on Colin)
|
||||||
|
public const LITELLM_SONNET = 'claude-sonnet-bedrock';
|
||||||
|
public const LITELLM_HAIKU = 'claude-haiku-bedrock';
|
||||||
|
// Opus — future premium tier; add 'claude-opus-bedrock' to litellm_config.yaml on Colin first
|
||||||
|
public const LITELLM_OPUS = 'claude-opus-bedrock';
|
||||||
|
|
||||||
|
// Actual Bedrock model IDs routed by LiteLLM (for reference)
|
||||||
|
public const BEDROCK_SONNET = 'eu.anthropic.claude-sonnet-4-6';
|
||||||
|
public const BEDROCK_HAIKU = 'eu.anthropic.claude-haiku-4-5-20251001-v1:0';
|
||||||
|
public const BEDROCK_OPUS = 'eu.anthropic.claude-opus-4-7'; // not yet in litellm_config.yaml
|
||||||
|
|
||||||
|
// Models that support extended thinking (via LiteLLM thinking param passthrough)
|
||||||
|
private const THINKING_MODELS = [
|
||||||
|
self::LITELLM_SONNET,
|
||||||
|
// self::LITELLM_OPUS, // uncomment after claude-opus-bedrock added to litellm_config.yaml
|
||||||
|
];
|
||||||
|
|
||||||
|
// Tools pinned to Azure GPT-4o-mini regardless of DBN_BEDROCK_ENABLED.
|
||||||
|
// These are mechanical/structural — regex, date extraction, translation — no quality gain from Claude.
|
||||||
|
private const AZURE_PINNED = ['redact', 'translate', 'timeline', 'search-legal', 'search'];
|
||||||
|
|
||||||
|
// Tools routed to Claude Haiku 4.5 (fast, good Norwegian comprehension, 4x cheaper than Sonnet).
|
||||||
|
// Pro users escalate to Sonnet.
|
||||||
|
private const HAIKU_TOOLS = ['ask', 'summarize', 'timeline-deep', 'citations'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns ['gateway' => 'azure'|'bedrock', 'model' => string|null].
|
||||||
|
* gateway='azure' means always use Azure regardless of DBN_BEDROCK_ENABLED.
|
||||||
|
* gateway='bedrock' means use Bedrock when enabled, fall back to Azure when not.
|
||||||
|
* model is null for azure-pinned tools (caller uses DbnAzureOpenAiGateway directly).
|
||||||
|
*/
|
||||||
|
public static function routeForTool(string $tool, string $tier = 'free'): array
|
||||||
|
{
|
||||||
|
$tier = in_array($tier, ['free', 'plus', 'pro'], true) ? $tier : 'free';
|
||||||
|
|
||||||
|
if (in_array($tool, self::AZURE_PINNED, true)) {
|
||||||
|
return ['gateway' => 'azure', 'model' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($tool, self::HAIKU_TOOLS, true)) {
|
||||||
|
// Pro users get Sonnet for these tools; free/plus get Haiku
|
||||||
|
$model = ($tier === 'pro') ? self::LITELLM_SONNET : self::LITELLM_HAIKU;
|
||||||
|
return ['gateway' => 'bedrock', 'model' => $model];
|
||||||
|
}
|
||||||
|
|
||||||
|
// All drafting/reasoning tools → Sonnet (korrespond, legal-analysis, deep-research,
|
||||||
|
// barnevernet-analyze, discrepancy-find, advocate)
|
||||||
|
return ['gateway' => 'bedrock', 'model' => self::LITELLM_SONNET];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use routeForTool() — kept for any direct callers outside the factory. */
|
||||||
|
public static function modelForTool(string $tool, string $tier = 'free'): string
|
||||||
|
{
|
||||||
|
$route = self::routeForTool($tool, $tier);
|
||||||
|
return $route['model'] ?? self::LITELLM_SONNET;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function supportsThinking(string $modelName): bool
|
||||||
|
{
|
||||||
|
return in_array($modelName, self::THINKING_MODELS, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function maxTokensForTool(string $tool): int
|
||||||
|
{
|
||||||
|
return match ($tool) {
|
||||||
|
'deep-research', 'barnevernet-analyze', 'advocate' => 4000,
|
||||||
|
'legal-analysis', 'korrespond', 'discrepancy-find' => 3000,
|
||||||
|
default => 2000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
||||||
|
require_once __DIR__ . '/DbnBedrockGateway.php';
|
||||||
|
require_once __DIR__ . '/DbnBedrockModelRouter.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single choke point for LLM gateway selection.
|
||||||
|
* When DBN_BEDROCK_ENABLED=false (default), always returns Azure — zero risk.
|
||||||
|
* When true, routes through LiteLLM on Colin (same path nova-lite already uses).
|
||||||
|
* AWS credentials live only in LiteLLM config — no keys needed in .env here.
|
||||||
|
* Rollback: flip DBN_BEDROCK_ENABLED=false in .env — no code push needed.
|
||||||
|
*/
|
||||||
|
final class DbnGatewayFactory
|
||||||
|
{
|
||||||
|
public static function bedrockEnabled(): bool
|
||||||
|
{
|
||||||
|
return dbnToolsEnv('DBN_BEDROCK_ENABLED', 'false') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Bedrock gateway configured for the given LiteLLM model name,
|
||||||
|
* or the default from DBN_BEDROCK_CHAT_MODEL. Falls back to Azure when disabled.
|
||||||
|
*/
|
||||||
|
public static function make(?string $modelName = null): DbnAzureOpenAiGateway|DbnBedrockGateway
|
||||||
|
{
|
||||||
|
if (!self::bedrockEnabled()) {
|
||||||
|
return new DbnAzureOpenAiGateway();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DbnBedrockGateway([
|
||||||
|
'chat_model_name' => $modelName
|
||||||
|
?? (string)dbnToolsEnv('DBN_BEDROCK_CHAT_MODEL', DbnBedrockModelRouter::LITELLM_SONNET),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a gateway pre-configured with the best model for the given tool and tier.
|
||||||
|
*
|
||||||
|
* Three-tier routing:
|
||||||
|
* - Azure-pinned tools (redact, translate, timeline, search-legal) → always Azure, Bedrock flag ignored.
|
||||||
|
* - Haiku tools (ask, summarize, timeline-deep, citations) → Haiku when Bedrock on, Azure when off.
|
||||||
|
* - Sonnet tools (korrespond, legal-analysis, deep-research, etc.) → Sonnet when on, Azure when off.
|
||||||
|
*/
|
||||||
|
public static function makeForTool(
|
||||||
|
string $tool,
|
||||||
|
string $tier = 'free'
|
||||||
|
): DbnAzureOpenAiGateway|DbnBedrockGateway {
|
||||||
|
$route = DbnBedrockModelRouter::routeForTool($tool, $tier);
|
||||||
|
|
||||||
|
// Azure-pinned tools bypass the Bedrock flag entirely
|
||||||
|
if ($route['gateway'] === 'azure') {
|
||||||
|
return new DbnAzureOpenAiGateway();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bedrock tools fall back to Azure when disabled
|
||||||
|
if (!self::bedrockEnabled()) {
|
||||||
|
return new DbnAzureOpenAiGateway();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::make($route['model']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/bootstrap.php';
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
||||||
|
require_once __DIR__ . '/DbnGatewayFactory.php';
|
||||||
|
require_once __DIR__ . '/DbnBedrockModelRouter.php';
|
||||||
|
|
||||||
final class DbnDeepResearchAgent
|
final class DbnDeepResearchAgent
|
||||||
{
|
{
|
||||||
@@ -13,13 +15,13 @@ final class DbnDeepResearchAgent
|
|||||||
private const MIN_CHUNK_WORDS = 50;
|
private const MIN_CHUNK_WORDS = 50;
|
||||||
private const POOL_CAP = 30;
|
private const POOL_CAP = 30;
|
||||||
|
|
||||||
private DbnAzureOpenAiGateway $azure;
|
private DbnAzureOpenAiGateway|DbnBedrockGateway $azure;
|
||||||
private array $uploadVecs = [];
|
private array $uploadVecs = [];
|
||||||
private array $stepTimings = [];
|
private array $stepTimings = [];
|
||||||
|
|
||||||
public function __construct(?DbnAzureOpenAiGateway $azure = null)
|
public function __construct(DbnAzureOpenAiGateway|DbnBedrockGateway|null $azure = null)
|
||||||
{
|
{
|
||||||
$this->azure = $azure ?: new DbnAzureOpenAiGateway();
|
$this->azure = $azure ?: DbnGatewayFactory::makeForTool('deep-research');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(
|
public function run(
|
||||||
@@ -984,8 +986,10 @@ PROMPT;
|
|||||||
'dbn_legal' => 'dbn-legal-agent-v2',
|
'dbn_legal' => 'dbn-legal-agent-v2',
|
||||||
'dbn_legal_v3' => 'dbn-legal-agent-v3',
|
'dbn_legal_v3' => 'dbn-legal-agent-v3',
|
||||||
'azure_full' => 'gpt-4o',
|
'azure_full' => 'gpt-4o',
|
||||||
|
'claude_sonnet'=> 'Claude 3.5 Sonnet',
|
||||||
default => $this->azure->chatDeployment(),
|
default => $this->azure->chatDeployment(),
|
||||||
},
|
},
|
||||||
|
'thinking_trace'=> null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1118,8 +1122,9 @@ PROMPT;
|
|||||||
['role' => 'system', 'content' => 'You return valid JSON only. No markdown fences. Every legal claim must be supported by a source from the numbered list. Do not invent statute sections, case names, paragraph numbers, or dates. If no source supports a point, omit it entirely.'],
|
['role' => 'system', 'content' => 'You return valid JSON only. No markdown fences. Every legal claim must be supported by a source from the numbered list. Do not invent statute sections, case names, paragraph numbers, or dates. If no source supports a point, omit it entirely.'],
|
||||||
['role' => 'user', 'content' => $prompt],
|
['role' => 'user', 'content' => $prompt],
|
||||||
];
|
];
|
||||||
$synthTemp = ($advocateRole !== '') ? min($temperature, 0.20) : $temperature;
|
$synthTemp = ($advocateRole !== '') ? min($temperature, 0.20) : $temperature;
|
||||||
$opts = ['json' => true, 'temperature' => $synthTemp, 'max_tokens' => 4000, 'timeout' => 180];
|
$opts = ['json' => true, 'temperature' => $synthTemp, 'max_tokens' => 4000, 'timeout' => 180];
|
||||||
|
$thinkingTrace = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($engine === 'dbn_legal_v3') {
|
if ($engine === 'dbn_legal_v3') {
|
||||||
@@ -1137,6 +1142,28 @@ PROMPT;
|
|||||||
} elseif ($engine === 'azure_full') {
|
} elseif ($engine === 'azure_full') {
|
||||||
$raw = $this->azure->withDeployment('gpt-4o')->chatText($messages, $opts);
|
$raw = $this->azure->withDeployment('gpt-4o')->chatText($messages, $opts);
|
||||||
$deployLabel = 'gpt-4o';
|
$deployLabel = 'gpt-4o';
|
||||||
|
} elseif ($engine === 'claude_sonnet' || ($this->azure instanceof DbnBedrockGateway)) {
|
||||||
|
if (
|
||||||
|
$this->azure instanceof DbnBedrockGateway
|
||||||
|
&& dbnToolsEnv('DBN_BEDROCK_THINKING_ENABLED', 'false') === 'true'
|
||||||
|
&& DbnBedrockModelRouter::supportsThinking($this->azure->chatDeployment())
|
||||||
|
) {
|
||||||
|
// Extended thinking — Pro showcase
|
||||||
|
$thinkResult = $this->azure->chatWithThinking($messages, [
|
||||||
|
'max_tokens' => 16000,
|
||||||
|
'thinking_budget'=> (int)dbnToolsEnv('DBN_BEDROCK_THINKING_BUDGET', '8000'),
|
||||||
|
'timeout' => 300,
|
||||||
|
]);
|
||||||
|
$raw = $thinkResult['text'];
|
||||||
|
$thinkingTrace = $thinkResult['thinking'] ?? null;
|
||||||
|
$deployLabel = 'Claude 3.5 Sonnet (extended thinking)';
|
||||||
|
} else {
|
||||||
|
$raw = $this->azure->chatText($messages, $opts);
|
||||||
|
$thinkingTrace = null;
|
||||||
|
$deployLabel = $this->azure instanceof DbnBedrockGateway
|
||||||
|
? 'Claude 3.5 Sonnet'
|
||||||
|
: $this->azure->chatDeployment();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$raw = $this->azure->chatText($messages, $opts);
|
$raw = $this->azure->chatText($messages, $opts);
|
||||||
$deployLabel = $this->azure->chatDeployment();
|
$deployLabel = $this->azure->chatDeployment();
|
||||||
@@ -1157,8 +1184,9 @@ PROMPT;
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'json' => $json,
|
'json' => $json,
|
||||||
'deploy_label' => $deployLabel,
|
'deploy_label' => $deployLabel,
|
||||||
|
'thinking_trace'=> $thinkingTrace,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/bootstrap.php';
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
||||||
|
require_once __DIR__ . '/DbnGatewayFactory.php';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Document Discrepancy Finder Agent
|
* Document Discrepancy Finder Agent
|
||||||
@@ -24,12 +25,12 @@ final class DbnDiscrepancyAgent
|
|||||||
private const MAX_DOC_CHARS = 64000;
|
private const MAX_DOC_CHARS = 64000;
|
||||||
private const POOL_CAP = 20;
|
private const POOL_CAP = 20;
|
||||||
|
|
||||||
private DbnAzureOpenAiGateway $azure;
|
private DbnAzureOpenAiGateway|DbnBedrockGateway $azure;
|
||||||
private array $stepTimings = [];
|
private array $stepTimings = [];
|
||||||
|
|
||||||
public function __construct(?DbnAzureOpenAiGateway $azure = null)
|
public function __construct(DbnAzureOpenAiGateway|DbnBedrockGateway|null $azure = null)
|
||||||
{
|
{
|
||||||
$this->azure = $azure ?: new DbnAzureOpenAiGateway();
|
$this->azure = $azure ?: DbnGatewayFactory::makeForTool('discrepancy-find');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/bootstrap.php';
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
||||||
|
require_once __DIR__ . '/DbnGatewayFactory.php';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Korrespond — drafts replies or new correspondence to Norwegian authorities
|
* Korrespond — drafts replies or new correspondence to Norwegian authorities
|
||||||
@@ -56,11 +57,11 @@ final class DbnKorrespondAgent
|
|||||||
'other' => 'mottaker',
|
'other' => 'mottaker',
|
||||||
];
|
];
|
||||||
|
|
||||||
private DbnAzureOpenAiGateway $azure;
|
private DbnAzureOpenAiGateway|DbnBedrockGateway $azure;
|
||||||
|
|
||||||
public function __construct(?DbnAzureOpenAiGateway $azure = null)
|
public function __construct(DbnAzureOpenAiGateway|DbnBedrockGateway|null $azure = null)
|
||||||
{
|
{
|
||||||
$this->azure = $azure ?: new DbnAzureOpenAiGateway();
|
$this->azure = $azure ?: DbnGatewayFactory::makeForTool('korrespond');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Localized chrome/progress strings, keyed by user UI language. */
|
/** Localized chrome/progress strings, keyed by user UI language. */
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/bootstrap.php';
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
||||||
|
require_once __DIR__ . '/DbnGatewayFactory.php';
|
||||||
require_once __DIR__ . '/LegalTools.php';
|
require_once __DIR__ . '/LegalTools.php';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,12 +23,15 @@ final class DbnLegalAnalysisAgent
|
|||||||
private const LEGAL_TIMEOUT = 60;
|
private const LEGAL_TIMEOUT = 60;
|
||||||
private const LEGAL_MODEL = 'dbn-legal-agent-v3';
|
private const LEGAL_MODEL = 'dbn-legal-agent-v3';
|
||||||
|
|
||||||
private DbnAzureOpenAiGateway $azureMini;
|
private DbnAzureOpenAiGateway|DbnBedrockGateway $azureMini;
|
||||||
private DbnLegalToolsService $legalSvc;
|
private DbnLegalToolsService $legalSvc;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->azureMini = (new DbnAzureOpenAiGateway())->withDeployment('gpt-4o-mini');
|
// On Azure: gpt-4o-mini for extraction/synthesis. On Bedrock: factory picks Haiku/Sonnet.
|
||||||
|
$this->azureMini = DbnGatewayFactory::bedrockEnabled()
|
||||||
|
? DbnGatewayFactory::makeForTool('legal-analysis')
|
||||||
|
: (new DbnAzureOpenAiGateway())->withDeployment('gpt-4o-mini');
|
||||||
$this->legalSvc = new DbnLegalToolsService();
|
$this->legalSvc = new DbnLegalToolsService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/bootstrap.php';
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
require_once __DIR__ . '/AzureOpenAiGateway.php';
|
||||||
|
require_once __DIR__ . '/DbnGatewayFactory.php';
|
||||||
|
|
||||||
final class DbnLegalToolsService
|
final class DbnLegalToolsService
|
||||||
{
|
{
|
||||||
private const MAX_PASTE_CHARS = 128000;
|
private const MAX_PASTE_CHARS = 128000;
|
||||||
private const MAX_TIMELINE_CHARS = 600000;
|
private const MAX_TIMELINE_CHARS = 600000;
|
||||||
|
|
||||||
private DbnAzureOpenAiGateway $azure;
|
private DbnAzureOpenAiGateway|DbnBedrockGateway $azure;
|
||||||
|
|
||||||
public function __construct(?DbnAzureOpenAiGateway $azure = null)
|
public function __construct(DbnAzureOpenAiGateway|DbnBedrockGateway|null $azure = null)
|
||||||
{
|
{
|
||||||
$this->azure = $azure ?: new DbnAzureOpenAiGateway();
|
$this->azure = $azure ?: DbnGatewayFactory::make();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function search(
|
public function search(
|
||||||
|
|||||||
+25
-22
@@ -21,7 +21,7 @@ final class ToolModels
|
|||||||
|
|
||||||
public static function engineForUser(int $userId, string $requestedEngine): string
|
public static function engineForUser(int $userId, string $requestedEngine): string
|
||||||
{
|
{
|
||||||
$valid = ['nova_lite', 'azure_mini', 'azure_full', 'gpu', 'regex'];
|
$valid = ['nova_lite', 'azure_mini', 'azure_full', 'gpu', 'regex', 'claude_haiku', 'claude_sonnet'];
|
||||||
$requestedEngine = in_array($requestedEngine, $valid, true) ? $requestedEngine : 'azure_mini';
|
$requestedEngine = in_array($requestedEngine, $valid, true) ? $requestedEngine : 'azure_mini';
|
||||||
|
|
||||||
if ($userId <= 0) {
|
if ($userId <= 0) {
|
||||||
@@ -37,7 +37,7 @@ final class ToolModels
|
|||||||
|
|
||||||
public static function timelineRoute(int $userId, string $requestedEngine, string $text): array
|
public static function timelineRoute(int $userId, string $requestedEngine, string $text): array
|
||||||
{
|
{
|
||||||
$valid = ['nova_lite', 'azure_mini', 'azure_full'];
|
$valid = ['nova_lite', 'azure_mini', 'azure_full', 'claude_haiku', 'claude_sonnet'];
|
||||||
$requestedEngine = in_array($requestedEngine, $valid, true) ? $requestedEngine : 'azure_mini';
|
$requestedEngine = in_array($requestedEngine, $valid, true) ? $requestedEngine : 'azure_mini';
|
||||||
$tierEngine = self::engineForUser($userId, $requestedEngine);
|
$tierEngine = self::engineForUser($userId, $requestedEngine);
|
||||||
$charCount = mb_strlen($text, 'UTF-8');
|
$charCount = mb_strlen($text, 'UTF-8');
|
||||||
@@ -122,60 +122,63 @@ final class ToolModels
|
|||||||
public static function timelineEngineLimit(string $engine): int
|
public static function timelineEngineLimit(string $engine): int
|
||||||
{
|
{
|
||||||
return match ($engine) {
|
return match ($engine) {
|
||||||
'nova_lite' => self::TIMELINE_QUICK_CHAR_LIMIT,
|
'nova_lite', 'claude_haiku' => self::TIMELINE_QUICK_CHAR_LIMIT,
|
||||||
'azure_mini' => self::TIMELINE_STANDARD_CHAR_LIMIT,
|
'azure_mini' => self::TIMELINE_STANDARD_CHAR_LIMIT,
|
||||||
default => self::TIMELINE_DEEP_CHAR_LIMIT,
|
default => self::TIMELINE_DEEP_CHAR_LIMIT,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function timelineChunkSize(string $engine): int
|
public static function timelineChunkSize(string $engine): int
|
||||||
{
|
{
|
||||||
return match ($engine) {
|
return match ($engine) {
|
||||||
'nova_lite' => 10000,
|
'nova_lite', 'claude_haiku' => 10000,
|
||||||
'azure_mini' => 16000,
|
'azure_mini' => 16000,
|
||||||
default => 30000,
|
default => 30000,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function timelineEngineMaxChars(string $engine): int
|
public static function timelineEngineMaxChars(string $engine): int
|
||||||
{
|
{
|
||||||
return match ($engine) {
|
return match ($engine) {
|
||||||
'nova_lite' => self::TIMELINE_QUICK_MAX_CHARS,
|
'nova_lite', 'claude_haiku' => self::TIMELINE_QUICK_MAX_CHARS,
|
||||||
'azure_mini' => self::TIMELINE_STANDARD_MAX_CHARS,
|
'azure_mini' => self::TIMELINE_STANDARD_MAX_CHARS,
|
||||||
default => self::TIMELINE_DEEP_MAX_CHARS,
|
default => self::TIMELINE_DEEP_MAX_CHARS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function timelineCreditsForSize(string $engine, int $charCount): int
|
public static function timelineCreditsForSize(string $engine, int $charCount): int
|
||||||
{
|
{
|
||||||
return match ($engine) {
|
return match ($engine) {
|
||||||
'nova_lite' => $charCount <= self::TIMELINE_QUICK_CHAR_LIMIT ? 1 : 2,
|
'nova_lite', 'claude_haiku'
|
||||||
'azure_mini' => $charCount <= self::TIMELINE_STANDARD_CHAR_LIMIT ? 1 : ($charCount <= 180000 ? 2 : 3),
|
=> $charCount <= self::TIMELINE_QUICK_CHAR_LIMIT ? 1 : 2,
|
||||||
default => $charCount <= self::TIMELINE_DEEP_CHAR_LIMIT ? 2 : ($charCount <= 350000 ? 4 : 6),
|
'azure_mini'
|
||||||
|
=> $charCount <= self::TIMELINE_STANDARD_CHAR_LIMIT ? 1 : ($charCount <= 180000 ? 2 : 3),
|
||||||
|
default
|
||||||
|
=> $charCount <= self::TIMELINE_DEEP_CHAR_LIMIT ? 2 : ($charCount <= 350000 ? 4 : 6),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function timelineAdvertisedCredits(string $engine): int
|
public static function timelineAdvertisedCredits(string $engine): int
|
||||||
{
|
{
|
||||||
return $engine === 'azure_full' ? 2 : 1;
|
return in_array($engine, ['azure_full', 'claude_sonnet'], true) ? 2 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function timelineEngineLabel(string $engine): string
|
public static function timelineEngineLabel(string $engine): string
|
||||||
{
|
{
|
||||||
return match ($engine) {
|
return match ($engine) {
|
||||||
'nova_lite' => 'Quick',
|
'nova_lite', 'claude_haiku' => 'Quick',
|
||||||
'azure_full' => 'Deep',
|
'azure_full', 'claude_sonnet' => 'Deep',
|
||||||
default => 'Standard',
|
default => 'Standard',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function timelineEngineRank(string $engine): int
|
private static function timelineEngineRank(string $engine): int
|
||||||
{
|
{
|
||||||
return match ($engine) {
|
return match ($engine) {
|
||||||
'nova_lite' => 1,
|
'nova_lite', 'claude_haiku' => 1,
|
||||||
'azure_mini' => 2,
|
'azure_mini' => 2,
|
||||||
'azure_full' => 3,
|
'azure_full', 'claude_sonnet' => 3,
|
||||||
default => 0,
|
default => 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user