Skip to content

Commit dc7de7e

Browse files
committed
wip
1 parent d7a06fc commit dc7de7e

File tree

15 files changed

+1241
-1049
lines changed

15 files changed

+1241
-1049
lines changed

config/cortex.php

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Cortex\LLM\Enums\LLMDriver;
66
use Cortex\LLM\Enums\StreamingProtocol;
77
use Cortex\Agents\Prebuilt\WeatherAgent;
8-
use Cortex\ModelInfo\Enums\ModelProvider;
98
use Cortex\ModelInfo\Providers\OllamaModelInfoProvider;
109
use Cortex\ModelInfo\Providers\LiteLLMModelInfoProvider;
1110
use Cortex\ModelInfo\Providers\LMStudioModelInfoProvider;
@@ -32,6 +31,7 @@
3231

3332
'openai' => [
3433
'driver' => LLMDriver::OpenAIChat,
34+
// 'driver' => LLMDriver::OpenAIResponses,
3535
'options' => [
3636
'api_key' => env('OPENAI_API_KEY', ''),
3737
'base_uri' => env('OPENAI_BASE_URI'),
@@ -40,23 +40,7 @@
4040
'default_model' => 'gpt-4.1-mini',
4141
'default_parameters' => [
4242
'temperature' => null,
43-
'max_tokens' => 1024,
44-
'top_p' => null,
45-
],
46-
],
47-
48-
'openai_responses' => [
49-
'driver' => LLMDriver::OpenAIResponses,
50-
'model_provider' => ModelProvider::OpenAI,
51-
'options' => [
52-
'api_key' => env('OPENAI_API_KEY', ''),
53-
'base_uri' => env('OPENAI_BASE_URI'),
54-
'organization' => env('OPENAI_ORGANIZATION'),
55-
],
56-
'default_model' => 'gpt-5-mini',
57-
'default_parameters' => [
58-
'temperature' => null,
59-
'max_tokens' => null,
43+
'max_output_tokens' => 1024,
6044
'top_p' => null,
6145
],
6246
],
@@ -75,42 +59,44 @@
7559
],
7660
],
7761

78-
'groq' => [
62+
'ollama' => [
7963
'driver' => LLMDriver::OpenAIChat,
64+
// 'driver' => LLMDriver::Anthropic,
8065
'options' => [
81-
'api_key' => env('GROQ_API_KEY', ''),
82-
'base_uri' => env('GROQ_BASE_URI', 'https://api.groq.com/openai/v1'),
66+
'api_key' => 'ollama',
67+
'base_uri' => env('OLLAMA_BASE_URI', 'http://localhost:11434/v1'),
8368
],
84-
'default_model' => 'llama-3.1-8b-instant',
69+
'default_model' => 'gpt-oss:20b',
8570
'default_parameters' => [
8671
'temperature' => null,
87-
'max_tokens' => null,
72+
'max_tokens' => 1024,
8873
'top_p' => null,
8974
],
9075
],
9176

92-
'ollama' => [
93-
'driver' => LLMDriver::OpenAIChat,
77+
'lmstudio' => [
78+
// 'driver' => LLMDriver::OpenAIChat,
79+
'driver' => LLMDriver::OpenAIResponses,
9480
// 'driver' => LLMDriver::Anthropic,
9581
'options' => [
96-
'api_key' => 'ollama',
97-
'base_uri' => env('OLLAMA_BASE_URI', 'http://localhost:11434/v1'),
82+
'api_key' => 'lmstudio',
83+
'base_uri' => env('LMSTUDIO_BASE_URI', 'http://localhost:1234/v1'),
9884
],
99-
'default_model' => 'gpt-oss:20b',
85+
'default_model' => 'openai/gpt-oss-20b',
10086
'default_parameters' => [
10187
'temperature' => null,
10288
'max_tokens' => 1024,
10389
'top_p' => null,
10490
],
10591
],
10692

107-
'lmstudio' => [
93+
'groq' => [
10894
'driver' => LLMDriver::OpenAIChat,
10995
'options' => [
110-
'api_key' => 'lmstudio',
111-
'base_uri' => env('LMSTUDIO_BASE_URI', 'http://localhost:1234/v1'),
96+
'api_key' => env('GROQ_API_KEY', ''),
97+
'base_uri' => env('GROQ_BASE_URI', 'https://api.groq.com/openai/v1'),
11298
],
113-
'default_model' => 'qwen2.5-14b-instruct-mlx',
99+
'default_model' => 'llama-3.1-8b-instant',
114100
'default_parameters' => [
115101
'temperature' => null,
116102
'max_tokens' => null,

src/Agents/Prebuilt/WeatherAgent.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public function llm(): LLM|string|null
5757
{
5858
return Cortex::llm('lmstudio', 'openai/gpt-oss-20b')->ignoreFeatures();
5959
// return Cortex::llm('anthropic', 'claude-haiku-4-5')->ignoreFeatures();
60+
// return Cortex::llm('openai', 'gpt-5-mini')->ignoreFeatures();
6061
}
6162

6263
#[Override]

src/Events/AgentStepError.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public function toArray(): array
2828
'run_id' => $this->config?->runId,
2929
'thread_id' => $this->config?->threadId,
3030
'agent' => $this->agent->getId(),
31+
'error' => $this->exception->getMessage(),
3132
];
3233
}
3334
}

src/Http/Controllers/AgentsController.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,11 @@ public function stream(string $agent, Request $request): StreamedResponse
101101
),
102102
);
103103

104+
/** @var StreamingProtocol $defaultProtocol */
105+
$defaultProtocol = config('cortex.default_streaming_protocol') ?? StreamingProtocol::Vercel;
106+
104107
return $result->streamResponse(
105-
$request->enum('protocol', StreamingProtocol::class, config('cortex.default_streaming_protocol')),
108+
$request->enum('protocol', StreamingProtocol::class, $defaultProtocol),
106109
);
107110
} catch (Throwable $e) {
108111
dd($e);

src/LLM/Drivers/OpenAI/Responses/Concerns/MapsMessages.php

Lines changed: 126 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -27,62 +27,68 @@ trait MapsMessages
2727
protected function mapMessagesForInput(MessageCollection $messages): array
2828
{
2929
return $messages
30-
->map(function (Message $message): array {
31-
// Handle ToolMessage specifically for Responses API
32-
if ($message instanceof ToolMessage) {
33-
return [
34-
'role' => $message->role->value,
35-
'content' => $message->content(),
36-
'tool_call_id' => $message->id,
37-
];
38-
}
39-
40-
// Handle AssistantMessage with tool calls
41-
if ($message instanceof AssistantMessage && $message->toolCalls?->isNotEmpty()) {
42-
$baseMessage = [
43-
'role' => $message->role->value,
44-
];
45-
46-
// Add content if present
47-
if ($message->content() !== null) {
48-
$baseMessage['content'] = $message->content();
49-
}
50-
51-
// Add tool calls
52-
$baseMessage['tool_calls'] = $message->toolCalls->map(function (ToolCall $toolCall): array {
53-
return [
54-
'id' => $toolCall->id,
55-
'type' => 'function',
56-
'function' => [
57-
'name' => $toolCall->function->name,
58-
'arguments' => json_encode($toolCall->function->arguments),
59-
],
60-
];
61-
})->toArray();
62-
63-
return $baseMessage;
64-
}
65-
66-
// Handle all other message types
67-
$formattedMessage = [
30+
->map(fn(Message $message): array => match (true) {
31+
$message instanceof ToolMessage => $this->mapToolMessage($message),
32+
$message instanceof AssistantMessage && $message->toolCalls?->isNotEmpty() => $this->mapAssistantMessageWithToolCalls($message),
33+
default => [
6834
'role' => $message->role()->value,
6935
'content' => $this->mapContentForInput($message->content()),
70-
];
71-
72-
return $formattedMessage;
36+
],
7337
})
7438
->values()
7539
->toArray();
7640
}
7741

7842
/**
79-
* Map content to the OpenAI Responses API format.
43+
* @return array<string, mixed>
44+
*/
45+
private function mapToolMessage(ToolMessage $message): array
46+
{
47+
return [
48+
'role' => $message->role()->value,
49+
'content' => $message->content(),
50+
'tool_call_id' => $message->id,
51+
];
52+
}
53+
54+
/**
55+
* @return array<string, mixed>
56+
*/
57+
private function mapAssistantMessageWithToolCalls(AssistantMessage $message): array
58+
{
59+
$mapped = [
60+
'role' => $message->role()->value,
61+
];
62+
63+
$content = $this->mapAssistantContentForInput($message->content());
64+
65+
if ($content !== []) {
66+
$mapped['content'] = $content;
67+
}
68+
69+
$mapped['tool_calls'] = $message->toolCalls->map(fn(ToolCall $toolCall): array => [
70+
'id' => $toolCall->id,
71+
'type' => 'function',
72+
'function' => [
73+
'name' => $toolCall->function->name,
74+
'arguments' => json_encode($toolCall->function->arguments),
75+
],
76+
])->toArray();
77+
78+
return $mapped;
79+
}
80+
81+
/**
82+
* Map assistant message content for the Responses API input.
83+
*
84+
* ReasoningContent is excluded because the Responses API does not
85+
* accept it back in the `input` field of subsequent requests.
8086
*
8187
* @param string|array<int, \Cortex\LLM\Contracts\Content>|null $content
8288
*
8389
* @return array<int, array<string, mixed>>
8490
*/
85-
protected function mapContentForInput(string|array|null $content): array
91+
protected function mapAssistantContentForInput(string|array|null $content): array
8692
{
8793
if ($content === null) {
8894
return [];
@@ -91,57 +97,99 @@ protected function mapContentForInput(string|array|null $content): array
9197
if (is_string($content)) {
9298
return [
9399
[
94-
'type' => 'input_text',
100+
'type' => 'output_text',
95101
'text' => $content,
96102
],
97103
];
98104
}
99105

100-
return array_map(function (mixed $item): array {
101-
if ($item instanceof ImageContent) {
102-
$this->supportsFeatureOrFail(ModelFeature::Vision);
106+
$mapped = [];
103107

104-
return [
105-
'type' => 'input_image',
106-
'image_url' => $item->url,
107-
'detail' => 'auto', // Default detail level
108-
];
108+
foreach ($content as $item) {
109+
// Skip reasoning content — the Responses API doesn't accept it in input
110+
if ($item instanceof ReasoningContent) {
111+
continue;
109112
}
110113

111-
if ($item instanceof AudioContent) {
112-
$this->supportsFeatureOrFail(ModelFeature::AudioInput);
113-
114-
return [
115-
'type' => 'input_audio',
116-
'data' => $item->base64Data,
117-
'format' => $item->format,
114+
if ($item instanceof TextContent) {
115+
$mapped[] = [
116+
'type' => 'output_text',
117+
'text' => $item->text ?? '',
118118
];
119119
}
120+
}
120121

121-
if ($item instanceof FileContent) {
122-
return [
123-
'type' => 'input_file',
124-
'file_id' => $item->fileName, // Assuming file_id should be the fileName
125-
];
126-
}
122+
return $mapped;
123+
}
127124

128-
if ($item instanceof TextContent) {
129-
return [
125+
/**
126+
* Map content to the OpenAI Responses API format.
127+
*
128+
* @param string|array<int, \Cortex\LLM\Contracts\Content>|null $content
129+
*
130+
* @return array<int, array<string, mixed>>
131+
*/
132+
protected function mapContentForInput(string|array|null $content): array
133+
{
134+
if ($content === null) {
135+
return [];
136+
}
137+
138+
if (is_string($content)) {
139+
return [
140+
[
130141
'type' => 'input_text',
131-
'text' => $item->text ?? '',
132-
];
133-
}
142+
'text' => $content,
143+
],
144+
];
145+
}
134146

135-
// Handle ReasoningContent and ToolContent
136-
if ($item instanceof ReasoningContent) {
137-
return [
147+
return array_map(function (mixed $item): array {
148+
return match (true) {
149+
$item instanceof ImageContent => $this->mapImageContent($item),
150+
$item instanceof AudioContent => $this->mapAudioContent($item),
151+
$item instanceof FileContent => [
152+
'type' => 'input_file',
153+
'file_id' => $item->fileName,
154+
],
155+
$item instanceof TextContent => [
156+
'type' => 'input_text',
157+
'text' => $item->text ?? '',
158+
],
159+
$item instanceof ReasoningContent => [
138160
'type' => 'input_text',
139161
'text' => $item->reasoning,
140-
];
141-
}
142-
143-
// Fallback for unknown content types
144-
return [];
162+
],
163+
default => [],
164+
};
145165
}, $content);
146166
}
167+
168+
/**
169+
* @return array<string, mixed>
170+
*/
171+
private function mapImageContent(ImageContent $item): array
172+
{
173+
$this->supportsFeatureOrFail(ModelFeature::Vision);
174+
175+
return [
176+
'type' => 'input_image',
177+
'image_url' => $item->url,
178+
'detail' => 'auto',
179+
];
180+
}
181+
182+
/**
183+
* @return array<string, mixed>
184+
*/
185+
private function mapAudioContent(AudioContent $item): array
186+
{
187+
$this->supportsFeatureOrFail(ModelFeature::AudioInput);
188+
189+
return [
190+
'type' => 'input_audio',
191+
'data' => $item->base64Data,
192+
'format' => $item->format,
193+
];
194+
}
147195
}

0 commit comments

Comments
 (0)