yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
browser_ai_service.cc
Go to the documentation of this file.
1#ifdef __EMSCRIPTEN__
2
4
5#include <emscripten.h>
6#include <iomanip>
7#include <sstream>
8
9#include "absl/strings/ascii.h"
10#include "absl/strings/match.h"
11#include "absl/strings/str_format.h"
12#include "absl/strings/str_join.h"
14#include "rom/rom.h" // Full definition needed for Rom member access
15
16namespace yaze {
17namespace cli {
18
19namespace {
20
21// Helper function to escape JSON strings
22std::string EscapeJson(const std::string& str) {
23 std::stringstream ss;
24 for (char c : str) {
25 switch (c) {
26 case '"':
27 ss << "\\\"";
28 break;
29 case '\\':
30 ss << "\\\\";
31 break;
32 case '\b':
33 ss << "\\b";
34 break;
35 case '\f':
36 ss << "\\f";
37 break;
38 case '\n':
39 ss << "\\n";
40 break;
41 case '\r':
42 ss << "\\r";
43 break;
44 case '\t':
45 ss << "\\t";
46 break;
47 default:
48 if (c < 0x20) {
49 ss << "\\u" << std::hex << std::setw(4) << std::setfill('0')
50 << static_cast<int>(c);
51 } else {
52 ss << c;
53 }
54 break;
55 }
56 }
57 return ss.str();
58}
59
60// Helper to convert chat history to Gemini format
61std::string ConvertHistoryToGeminiFormat(
62 const std::vector<agent::ChatMessage>& history) {
63 nlohmann::json contents = nlohmann::json::array();
64
65 for (const auto& msg : history) {
66 nlohmann::json part;
67 part["text"] = msg.message;
68
69 nlohmann::json content;
70 content["parts"] = nlohmann::json::array({part});
71 content["role"] =
72 (msg.sender == agent::ChatMessage::Sender::kUser) ? "user" : "model";
73
74 contents.push_back(content);
75 }
76
77 return contents.dump();
78}
79
80bool IsOpenAiCompatibleProvider(const std::string& provider) {
81 return provider == kProviderOpenAi || provider == kProviderLmStudio ||
82 provider == kProviderLmStudioDashed ||
83 provider == kProviderCustomOpenAi ||
84 provider == kProviderOpenAiCompatible || provider == kProviderHalext ||
85 provider == kProviderAfsBridge;
86}
87
88std::string NormalizeBrowserProvider(std::string provider) {
89 provider = absl::AsciiStrToLower(provider);
90 if (provider.empty()) {
91 return kProviderGemini;
92 }
93 if (provider == kProviderGoogle || provider == kProviderGoogleGemini) {
94 return kProviderGemini;
95 }
96 if (provider == kProviderLmStudioDashed) {
97 return kProviderLmStudio;
98 }
99 if (provider == kProviderCustomOpenAi ||
100 provider == kProviderOpenAiCompatible) {
101 return kProviderOpenAi;
102 }
103 return provider;
104}
105
106bool IsLikelyLocalApiBase(const std::string& base) {
107 const std::string lower = absl::AsciiStrToLower(base);
108 return absl::StrContains(lower, "localhost") ||
109 absl::StrContains(lower, "127.0.0.1") ||
110 absl::StrContains(lower, "0.0.0.0") || absl::StrContains(lower, "::1");
111}
112
113std::string InferModelFamily(const std::string& model_name) {
114 if (model_name.empty()) {
115 return {};
116 }
117 const size_t slash = model_name.find('/');
118 const size_t dash = model_name.find('-');
119 const size_t delimiter = std::min(slash, dash);
120 if (delimiter != std::string::npos) {
121 return model_name.substr(0, delimiter);
122 }
123 return model_name;
124}
125
126void AddCurrentModelFallback(std::vector<ModelInfo>* models,
127 const std::string& provider,
128 const std::string& model_name, bool is_local,
129 const std::string& description) {
130 if (!models || model_name.empty()) {
131 return;
132 }
133 for (const auto& model : *models) {
134 if (model.name == model_name) {
135 return;
136 }
137 }
138 models->insert(models->begin(), {.name = model_name,
139 .display_name = model_name,
140 .provider = provider,
141 .description = description,
142 .family = InferModelFamily(model_name),
143 .is_local = is_local});
144}
145
146} // namespace
147
148BrowserAIService::BrowserAIService(
149 const BrowserAIConfig& config,
150 std::unique_ptr<net::IHttpClient> http_client)
151 : config_(config),
152 base_system_instruction_(config.system_instruction),
153 http_client_(std::move(http_client)) {
154 // Normalize provider name
155 config_.provider = NormalizeBrowserProvider(config_.provider);
156 // Set sensible defaults per provider
157 if (config_.provider == kProviderOpenAi) {
158 if (config_.model.empty()) {
159 config_.model = "gpt-4o-mini";
160 }
161 if (config_.api_base.empty()) {
162 config_.api_base = kOpenAIApiBaseUrl;
163 }
164 } else if (config_.provider == kProviderLmStudio) {
165 if (config_.api_base.empty()) {
166 config_.api_base = "http://localhost:1234/v1";
167 }
168 } else if (config_.provider == kProviderHalext ||
169 config_.provider == kProviderAfsBridge) {
170 if (config_.api_base.empty()) {
171 config_.api_base = "https://halext.org/v1";
172 }
173 } else {
174 if (config_.model.empty()) {
175 config_.model = "gemini-2.5-flash";
176 }
177 }
178
179 if (!http_client_) {
180 // This shouldn't happen in normal usage but handle gracefully
181 LogDebug("Warning: No HTTP client provided to BrowserAIService");
182 }
183
184 // Set timeout on HTTP client
185 if (http_client_) {
186 http_client_->SetTimeout(config_.timeout_seconds);
187 }
188
189 LogDebug(absl::StrFormat("BrowserAIService initialized with model: %s",
190 config_.model));
191}
192
193void BrowserAIService::SetRomContext(Rom* rom) {
194 std::lock_guard<std::mutex> lock(mutex_);
195 rom_ = rom;
196 config_.system_instruction = base_system_instruction_;
197 if (rom_ && rom_->is_loaded()) {
198 const std::string rom_context = absl::StrFormat(
199 "The ROM file '%s' is currently loaded. Tailor advice to the active "
200 "project and loaded data.",
201 rom_->filename());
202 if (config_.system_instruction.empty()) {
203 config_.system_instruction =
204 "You are assisting with ROM hacking for a Zelda SNES project. " +
205 rom_context;
206 } else {
207 config_.system_instruction =
208 config_.system_instruction + "\n\n" + rom_context;
209 }
210 }
211}
212
213absl::StatusOr<AgentResponse> BrowserAIService::GenerateResponse(
214 const std::string& prompt) {
215 std::lock_guard<std::mutex> lock(mutex_);
216 if (!http_client_) {
217 return absl::FailedPreconditionError("HTTP client not initialized");
218 }
219
220 if (RequiresApiKey() && config_.api_key.empty()) {
221 if (IsOpenAiCompatibleProvider(config_.provider)) {
222 return absl::InvalidArgumentError(
223 "OpenAI-compatible API key not set. Provide a key for remote "
224 "endpoints, or use a local OpenAI-compatible server.");
225 }
226 return absl::InvalidArgumentError(
227 "API key not set. Please provide a Gemini API key.");
228 }
229
230 LogDebug(absl::StrFormat("Generating response for prompt: %s", prompt));
231
232 // Build API URL
233 std::string url = BuildApiUrl("generateContent");
234
235 // Build request body
236 std::string request_body;
237 if (IsOpenAiCompatibleProvider(config_.provider)) {
238 url = config_.api_base.empty() ? kOpenAIApiBaseUrl : config_.api_base;
239 url += "/chat/completions";
240 request_body = BuildOpenAIRequestBody(prompt, nullptr);
241 } else {
242 request_body = BuildRequestBody(prompt);
243 }
244
245 // Set headers
246 net::Headers headers;
247 headers["Content-Type"] = "application/json";
248 if (IsOpenAiCompatibleProvider(config_.provider) &&
249 !config_.api_key.empty()) {
250 headers["Authorization"] = "Bearer " + config_.api_key;
251 }
252
253 // Make API request
254 auto response_or = http_client_->Post(url, request_body, headers);
255 if (!response_or.ok()) {
256 return absl::InternalError(absl::StrFormat("Failed to make API request: %s",
257 response_or.status().message()));
258 }
259
260 const auto& response = response_or.value();
261
262 // Check HTTP status
263 if (!response.IsSuccess()) {
264 if (response.IsClientError()) {
265 return absl::InvalidArgumentError(
266 absl::StrFormat("API request failed with status %d: %s",
267 response.status_code, response.body));
268 } else {
269 return absl::InternalError(absl::StrFormat(
270 "API server error %d: %s", response.status_code, response.body));
271 }
272 }
273
274 // Parse response
275 if (IsOpenAiCompatibleProvider(config_.provider)) {
276 return ParseOpenAIResponse(response.body);
277 }
278 return ParseGeminiResponse(response.body);
279}
280
281absl::StatusOr<AgentResponse> BrowserAIService::GenerateResponse(
282 const std::vector<agent::ChatMessage>& history) {
283 std::lock_guard<std::mutex> lock(mutex_);
284 if (!http_client_) {
285 return absl::FailedPreconditionError("HTTP client not initialized");
286 }
287
288 if (RequiresApiKey() && config_.api_key.empty()) {
289 if (IsOpenAiCompatibleProvider(config_.provider)) {
290 return absl::InvalidArgumentError(
291 "OpenAI-compatible API key not set. Provide a key for remote "
292 "endpoints, or use a local OpenAI-compatible server.");
293 }
294 return absl::InvalidArgumentError(
295 "API key not set. Please provide a Gemini API key.");
296 }
297
298 if (history.empty()) {
299 return absl::InvalidArgumentError("Chat history cannot be empty");
300 }
301
302 LogDebug(
303 absl::StrFormat("Generating response from %zu messages", history.size()));
304
305 // Build API URL
306 std::string url = BuildApiUrl("generateContent");
307
308 std::string request_body;
309 if (IsOpenAiCompatibleProvider(config_.provider)) {
310 url = config_.api_base.empty() ? kOpenAIApiBaseUrl : config_.api_base;
311 url += "/chat/completions";
312 request_body = BuildOpenAIRequestBody("", &history);
313 } else {
314 // Convert history to Gemini format and build request
315 nlohmann::json request;
316 request["contents"] =
317 nlohmann::json::parse(ConvertHistoryToGeminiFormat(history));
318
319 // Add generation config
320 request["generationConfig"]["temperature"] = config_.temperature;
321 request["generationConfig"]["maxOutputTokens"] = config_.max_output_tokens;
322
323 // Add system instruction if provided
324 if (!config_.system_instruction.empty()) {
325 request["systemInstruction"]["parts"][0]["text"] =
326 config_.system_instruction;
327 }
328
329 request_body = request.dump();
330 }
331
332 // Set headers
333 net::Headers headers;
334 headers["Content-Type"] = "application/json";
335 if (IsOpenAiCompatibleProvider(config_.provider) &&
336 !config_.api_key.empty()) {
337 headers["Authorization"] = "Bearer " + config_.api_key;
338 }
339
340 // Make API request
341 auto response_or = http_client_->Post(url, request_body, headers);
342 if (!response_or.ok()) {
343 return absl::InternalError(absl::StrFormat("Failed to make API request: %s",
344 response_or.status().message()));
345 }
346
347 const auto& response = response_or.value();
348
349 // Check HTTP status
350 if (!response.IsSuccess()) {
351 return absl::InternalError(
352 absl::StrFormat("API request failed with status %d: %s",
353 response.status_code, response.body));
354 }
355
356 // Parse response
357 if (IsOpenAiCompatibleProvider(config_.provider)) {
358 return ParseOpenAIResponse(response.body);
359 }
360 return ParseGeminiResponse(response.body);
361}
362
363absl::StatusOr<std::vector<ModelInfo>> BrowserAIService::ListAvailableModels() {
364 std::lock_guard<std::mutex> lock(mutex_);
365 std::vector<ModelInfo> models;
366
367 const std::string provider =
368 config_.provider.empty() ? kProviderGemini : config_.provider;
369
370 if (IsOpenAiCompatibleProvider(provider)) {
371 const std::string base = GetOpenAIApiBase();
372 const bool is_local = IsLikelyLocalApiBase(base);
373 const bool is_official_openai =
374 provider == kProviderOpenAi && base == kOpenAIApiBaseUrl;
375 if (http_client_) {
376 net::Headers headers;
377 if (!config_.api_key.empty()) {
378 headers["Authorization"] = "Bearer " + config_.api_key;
379 }
380 auto response_or = http_client_->Get(base + "/models", headers);
381 if (response_or.ok() && response_or->IsSuccess()) {
382 auto json = nlohmann::json::parse(response_or->body, nullptr, false);
383 if (json.is_object() && json.contains("data") &&
384 json["data"].is_array()) {
385 for (const auto& entry : json["data"]) {
386 if (!entry.is_object() || !entry.contains("id") ||
387 !entry["id"].is_string()) {
388 continue;
389 }
390 const std::string id = entry["id"].get<std::string>();
391 std::string description =
392 is_local ? "Discovered from local OpenAI-compatible endpoint"
393 : "Discovered from OpenAI-compatible endpoint";
394 if (entry.contains("owned_by") && entry["owned_by"].is_string()) {
395 const std::string owner = entry["owned_by"].get<std::string>();
396 description = absl::StrFormat("Owned by %s", owner);
397 }
398 models.push_back({.name = id,
399 .display_name = id,
400 .provider = provider,
401 .description = description,
402 .family = InferModelFamily(id),
403 .is_local = is_local});
404 }
405 if (!models.empty()) {
406 AddCurrentModelFallback(&models, provider, config_.model, is_local,
407 is_local
408 ? "Configured local model"
409 : "Configured OpenAI-compatible model");
410 return models;
411 }
412 }
413 }
414 }
415
416 if (!is_local || !config_.model.empty()) {
417 AddCurrentModelFallback(&models, provider, config_.model, is_local,
418 is_local ? "Configured local model"
419 : "Configured OpenAI-compatible model");
420 }
421 if (is_official_openai) {
422 models.push_back({.name = "gpt-4o-mini",
423 .display_name = "GPT-4o Mini",
424 .provider = provider,
425 .description = "Fast/cheap OpenAI model",
426 .family = "gpt-4o",
427 .is_local = false});
428 models.push_back({.name = "gpt-4o",
429 .display_name = "GPT-4o",
430 .provider = provider,
431 .description = "Balanced OpenAI flagship model",
432 .family = "gpt-4o",
433 .is_local = false});
434 models.push_back({.name = "gpt-4.1-mini",
435 .display_name = "GPT-4.1 Mini",
436 .provider = provider,
437 .description = "Lightweight 4.1 variant",
438 .family = "gpt-4.1",
439 .is_local = false});
440 }
441 } else {
442 models.push_back(
443 {.name = "gemini-2.5-flash",
444 .display_name = "Gemini 2.0 Flash (Experimental)",
445 .provider = kProviderGemini,
446 .description = "Fastest Gemini model with experimental features",
447 .family = "gemini",
448 .is_local = false});
449
450 models.push_back({.name = "gemini-1.5-flash",
451 .display_name = "Gemini 1.5 Flash",
452 .provider = kProviderGemini,
453 .description = "Fast and efficient for most tasks",
454 .family = "gemini",
455 .is_local = false});
456
457 models.push_back({.name = "gemini-1.5-flash-8b",
458 .display_name = "Gemini 1.5 Flash 8B",
459 .provider = kProviderGemini,
460 .description = "Smaller, faster variant of Flash",
461 .family = "gemini",
462 .parameter_size = "8B",
463 .is_local = false});
464
465 models.push_back({.name = "gemini-1.5-pro",
466 .display_name = "Gemini 1.5 Pro",
467 .provider = kProviderGemini,
468 .description = "Most capable model for complex tasks",
469 .family = "gemini",
470 .is_local = false});
471 }
472
473 return models;
474}
475
476absl::StatusOr<AgentResponse> BrowserAIService::AnalyzeImage(
477 const std::string& image_data, const std::string& prompt) {
478 std::lock_guard<std::mutex> lock(mutex_);
479 if (!http_client_) {
480 return absl::FailedPreconditionError("HTTP client not initialized");
481 }
482
483 if (IsOpenAiCompatibleProvider(config_.provider)) {
484 return absl::UnimplementedError(
485 "Image analysis not yet supported for OpenAI-compatible providers in "
486 "the WASM build");
487 }
488
489 if (config_.api_key.empty()) {
490 return absl::InvalidArgumentError(
491 "API key not set. Please provide a Gemini API key.");
492 }
493
494 LogDebug(absl::StrFormat("Analyzing image with prompt: %s", prompt));
495
496 // Build API URL
497 std::string url = BuildApiUrl("generateContent");
498
499 // Determine MIME type from image data prefix if present
500 std::string mime_type = "image/png"; // Default
501 if (image_data.find("data:image/jpeg") == 0 ||
502 image_data.find("data:image/jpg") == 0) {
503 mime_type = "image/jpeg";
504 }
505
506 // Strip data URL prefix if present
507 std::string clean_image_data = image_data;
508 size_t comma_pos = image_data.find(',');
509 if (comma_pos != std::string::npos && image_data.find("data:") == 0) {
510 clean_image_data = image_data.substr(comma_pos + 1);
511 }
512
513 // Build multimodal request
514 std::string request_body =
515 BuildMultimodalRequestBody(prompt, clean_image_data, mime_type);
516
517 // Set headers
518 net::Headers headers;
519 headers["Content-Type"] = "application/json";
520
521 // Make API request
522 auto response_or = http_client_->Post(url, request_body, headers);
523 if (!response_or.ok()) {
524 return absl::InternalError(absl::StrFormat("Failed to make API request: %s",
525 response_or.status().message()));
526 }
527
528 const auto& response = response_or.value();
529
530 // Check HTTP status
531 if (!response.IsSuccess()) {
532 return absl::InternalError(
533 absl::StrFormat("API request failed with status %d: %s",
534 response.status_code, response.body));
535 }
536
537 // Parse response
538 return ParseGeminiResponse(response.body);
539}
540
541absl::Status BrowserAIService::CheckAvailability() {
542 std::lock_guard<std::mutex> lock(mutex_);
543 if (!http_client_) {
544 return absl::FailedPreconditionError("HTTP client not initialized");
545 }
546
547 if (RequiresApiKey() && config_.api_key.empty()) {
548 if (config_.provider == kProviderOpenAi) {
549 return absl::InvalidArgumentError(
550 "OpenAI API key not set. Provide a key for https://api.openai.com, "
551 "or use a local OpenAI-compatible endpoint.");
552 }
553 return absl::InvalidArgumentError("Gemini API key not set");
554 }
555
556 net::Headers headers;
557 std::string url;
558
559 if (IsOpenAiCompatibleProvider(config_.provider)) {
560 url = GetOpenAIApiBase();
561 url += "/models";
562 if (!config_.api_key.empty()) {
563 headers["Authorization"] = "Bearer " + config_.api_key;
564 }
565 } else {
566 url = absl::StrFormat("%s%s?key=%s", kGeminiApiBaseUrl, config_.model,
567 config_.api_key);
568 }
569
570 auto response_or = http_client_->Get(url, headers);
571
572 if (!response_or.ok()) {
573 return absl::UnavailableError(
574 absl::StrFormat("Cannot reach %s API: %s", config_.provider,
575 response_or.status().message()));
576 }
577
578 const auto& response = response_or.value();
579 if (!response.IsSuccess()) {
580 if (response.status_code == 401 || response.status_code == 403) {
581 return absl::PermissionDeniedError("Invalid API key");
582 }
583 return absl::UnavailableError(absl::StrFormat(
584 "%s API returned error %d", config_.provider, response.status_code));
585 }
586
587 return absl::OkStatus();
588}
589
590void BrowserAIService::UpdateApiKey(const std::string& api_key) {
591 std::lock_guard<std::mutex> lock(mutex_);
592 config_.api_key = api_key;
593
594 // Store in sessionStorage for this session
595 // Note: This is handled by the secure storage module
596 LogDebug("API key updated");
597}
598
599bool BrowserAIService::RequiresApiKey() const {
600 if (IsOpenAiCompatibleProvider(config_.provider)) {
601 return !IsLikelyLocalApiBase(GetOpenAIApiBase());
602 }
603 return true;
604}
605
606std::string BrowserAIService::GetOpenAIApiBase() const {
607 std::string base =
608 config_.api_base.empty() ? kOpenAIApiBaseUrl : config_.api_base;
609 if (!base.empty() && base.back() == '/') {
610 base.pop_back();
611 }
612 return base;
613}
614
615std::string BrowserAIService::BuildApiUrl(const std::string& endpoint) const {
616 if (IsOpenAiCompatibleProvider(config_.provider)) {
617 std::string base = GetOpenAIApiBase();
618 return absl::StrFormat("%s/%s", base, endpoint);
619 }
620
621 return absl::StrFormat("%s%s:%s?key=%s", kGeminiApiBaseUrl, config_.model,
622 endpoint, config_.api_key);
623}
624
625std::string BrowserAIService::BuildRequestBody(const std::string& prompt,
626 bool include_system) const {
627 nlohmann::json request;
628
629 // Build contents array with user prompt
630 nlohmann::json user_part;
631 user_part["text"] = prompt;
632
633 nlohmann::json user_content;
634 user_content["parts"] = nlohmann::json::array({user_part});
635 user_content["role"] = "user";
636
637 request["contents"] = nlohmann::json::array({user_content});
638
639 // Add generation config
640 request["generationConfig"]["temperature"] = config_.temperature;
641 request["generationConfig"]["maxOutputTokens"] = config_.max_output_tokens;
642
643 // Add system instruction if provided and requested
644 if (include_system && !config_.system_instruction.empty()) {
645 nlohmann::json system_part;
646 system_part["text"] = config_.system_instruction;
647 request["systemInstruction"]["parts"] =
648 nlohmann::json::array({system_part});
649 }
650
651 return request.dump();
652}
653
654std::string BrowserAIService::BuildMultimodalRequestBody(
655 const std::string& prompt, const std::string& image_data,
656 const std::string& mime_type) const {
657 nlohmann::json request;
658
659 // Build parts array with text and image
660 nlohmann::json text_part;
661 text_part["text"] = prompt;
662
663 nlohmann::json image_part;
664 image_part["inline_data"]["mime_type"] = mime_type;
665 image_part["inline_data"]["data"] = image_data;
666
667 nlohmann::json content;
668 content["parts"] = nlohmann::json::array({text_part, image_part});
669 content["role"] = "user";
670
671 request["contents"] = nlohmann::json::array({content});
672
673 // Add generation config
674 request["generationConfig"]["temperature"] = config_.temperature;
675 request["generationConfig"]["maxOutputTokens"] = config_.max_output_tokens;
676
677 // Add system instruction if provided
678 if (!config_.system_instruction.empty()) {
679 nlohmann::json system_part;
680 system_part["text"] = config_.system_instruction;
681 request["systemInstruction"]["parts"] =
682 nlohmann::json::array({system_part});
683 }
684
685 return request.dump();
686}
687
688std::string BrowserAIService::BuildOpenAIRequestBody(
689 const std::string& prompt,
690 const std::vector<agent::ChatMessage>* history) const {
691 nlohmann::json request;
692 request["model"] = config_.model.empty() ? "gpt-4o-mini" : config_.model;
693
694 nlohmann::json messages = nlohmann::json::array();
695 if (!config_.system_instruction.empty()) {
696 messages.push_back(
697 {{"role", "system"}, {"content", config_.system_instruction}});
698 }
699
700 if (history && !history->empty()) {
701 for (const auto& msg : *history) {
702 messages.push_back(
703 {{"role", msg.sender == agent::ChatMessage::Sender::kUser
704 ? "user"
705 : "assistant"},
706 {"content", msg.message}});
707 }
708 } else if (!prompt.empty()) {
709 messages.push_back({{"role", "user"}, {"content", prompt}});
710 }
711
712 request["messages"] = messages;
713 request["temperature"] = config_.temperature;
714 request["max_tokens"] = config_.max_output_tokens;
715
716 return request.dump();
717}
718
719absl::StatusOr<AgentResponse> BrowserAIService::ParseGeminiResponse(
720 const std::string& response_body) const {
721 try {
722 nlohmann::json json = nlohmann::json::parse(response_body);
723
724 // Check for API errors
725 auto error_status = CheckForApiError(json);
726 if (!error_status.ok()) {
727 return error_status;
728 }
729
730 // Extract text from candidates
731 std::string text_content = ExtractTextFromCandidates(json);
732
733 if (text_content.empty()) {
734 return absl::InternalError("Empty response from Gemini API");
735 }
736
737 // Build agent response
738 AgentResponse response;
739 response.text_response = text_content;
740 response.provider = kProviderGemini;
741 response.model = config_.model;
742
743 // Add any safety ratings or filters as warnings
744 if (json.contains("promptFeedback") &&
745 json["promptFeedback"].contains("safetyRatings")) {
746 for (const auto& rating : json["promptFeedback"]["safetyRatings"]) {
747 if (rating.contains("probability") &&
748 rating["probability"] != "NEGLIGIBLE" &&
749 rating["probability"] != "LOW") {
750 response.warnings.push_back(absl::StrFormat(
751 "Content flagged: %s (%s)", rating.value("category", "unknown"),
752 rating.value("probability", "unknown")));
753 }
754 }
755 }
756
757 LogDebug(absl::StrFormat("Successfully parsed response with %zu characters",
758 text_content.length()));
759
760 return response;
761
762 } catch (const nlohmann::json::exception& e) {
763 return absl::InternalError(
764 absl::StrFormat("Failed to parse Gemini response: %s", e.what()));
765 }
766}
767
768absl::StatusOr<AgentResponse> BrowserAIService::ParseOpenAIResponse(
769 const std::string& response_body) const {
770 try {
771 nlohmann::json json = nlohmann::json::parse(response_body);
772
773 if (json.contains("error")) {
774 const auto& err = json["error"];
775 std::string message = err.value("message", "Unknown error");
776 int code = err.value("code", 0);
777 if (code == 401 || code == 403)
778 return absl::UnauthenticatedError(message);
779 if (code == 429)
780 return absl::ResourceExhaustedError(message);
781 return absl::InternalError(message);
782 }
783
784 if (!json.contains("choices") || !json["choices"].is_array() ||
785 json["choices"].empty()) {
786 return absl::InternalError("Empty response from OpenAI API");
787 }
788
789 const auto& choice = json["choices"][0];
790 if (!choice.contains("message") || !choice["message"].contains("content")) {
791 return absl::InternalError("Malformed OpenAI response");
792 }
793
794 std::string text = choice["message"]["content"].get<std::string>();
795 if (text.empty()) {
796 return absl::InternalError("OpenAI returned empty content");
797 }
798
799 AgentResponse response;
800 response.text_response = text;
801 response.provider = config_.provider;
802 response.model = config_.model;
803 return response;
804 } catch (const nlohmann::json::exception& e) {
805 return absl::InternalError(
806 absl::StrFormat("Failed to parse OpenAI response: %s", e.what()));
807 }
808}
809
810std::string BrowserAIService::ExtractTextFromCandidates(
811 const nlohmann::json& json) const {
812 if (!json.contains("candidates") || !json["candidates"].is_array() ||
813 json["candidates"].empty()) {
814 return "";
815 }
816
817 const auto& candidate = json["candidates"][0];
818
819 if (!candidate.contains("content") ||
820 !candidate["content"].contains("parts") ||
821 !candidate["content"]["parts"].is_array() ||
822 candidate["content"]["parts"].empty()) {
823 return "";
824 }
825
826 std::string result;
827 for (const auto& part : candidate["content"]["parts"]) {
828 if (part.contains("text")) {
829 result += part["text"].get<std::string>();
830 }
831 }
832
833 return result;
834}
835
836absl::Status BrowserAIService::CheckForApiError(
837 const nlohmann::json& json) const {
838 if (json.contains("error")) {
839 const auto& error = json["error"];
840 int code = error.value("code", 0);
841 std::string message = error.value("message", "Unknown error");
842 std::string status = error.value("status", "");
843
844 // Map common error codes to appropriate status codes
845 if (code == 400 || status == "INVALID_ARGUMENT") {
846 return absl::InvalidArgumentError(message);
847 } else if (code == 401 || status == "UNAUTHENTICATED") {
848 return absl::UnauthenticatedError(message);
849 } else if (code == 403 || status == "PERMISSION_DENIED") {
850 return absl::PermissionDeniedError(message);
851 } else if (code == 429 || status == "RESOURCE_EXHAUSTED") {
852 return absl::ResourceExhaustedError(message);
853 } else if (code == 503 || status == "UNAVAILABLE") {
854 return absl::UnavailableError(message);
855 } else {
856 return absl::InternalError(message);
857 }
858 }
859
860 return absl::OkStatus();
861}
862
863void BrowserAIService::LogDebug(const std::string& message) const {
864 if (config_.verbose) {
865 // Use console.log for browser debugging
866 EM_ASM(
867 { console.log('[BrowserAIService] ' + UTF8ToString($0)); },
868 message.c_str());
869 }
870}
871
872} // namespace cli
873} // namespace yaze
874
875#endif // __EMSCRIPTEN__
bool IsOpenAiCompatibleProvider(absl::string_view provider)
std::string EscapeJson(const std::string &input)
constexpr char kProviderGemini[]
Definition provider_ids.h:9
constexpr char kProviderGoogle[]
constexpr char kProviderCustomOpenAi[]
constexpr char kProviderHalext[]
constexpr char kProviderGoogleGemini[]
constexpr char kProviderOpenAiCompatible[]
constexpr char kProviderAfsBridge[]
constexpr char kProviderLmStudioDashed[]
constexpr char kProviderOpenAi[]
constexpr char kProviderLmStudio[]
Rom * rom()
Get the current ROM instance.