Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/genai/lib/interface/model_providers/anthropic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ class AnthropicModel extends ModelProvider {
),
body: kJsonEncoder.convert({
"model": aiRequestModel.model,
if (aiRequestModel.systemPrompt.isNotEmpty)
"system": aiRequestModel.systemPrompt,
"messages": [
{"role": "system", "content": aiRequestModel.systemPrompt},
{"role": "user", "content": aiRequestModel.userPrompt},
{
"role": "user",
"content": aiRequestModel.userPrompt.isNotEmpty
? aiRequestModel.userPrompt
: "Generate",
},
],
...aiRequestModel.getModelConfigMap(),
if (aiRequestModel.stream ?? false) ...{'stream': true},
Expand Down
168 changes: 168 additions & 0 deletions packages/genai/test/interface/model_providers/anthropic_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:genai/interface/consts.dart';
import 'package:genai/interface/model_providers/anthropic.dart';
import 'package:genai/models/ai_request_model.dart';

void main() {
group('AnthropicModel', () {
test('should return default AIRequestModel with Anthropic provider', () {
final defaultModel = AnthropicModel.instance.defaultAIRequestModel;

expect(defaultModel.modelApiProvider, equals(ModelAPIProvider.anthropic));
expect(defaultModel.url, equals(kAnthropicUrl));
});

test('should return null when aiRequestModel is null', () {
final result = AnthropicModel.instance.createRequest(null);
expect(result, isNull);
});

test('should set anthropic-version header', () {
const req = AIRequestModel(
modelApiProvider: ModelAPIProvider.anthropic,
url: kAnthropicUrl,
model: 'claude-3-5-sonnet-latest',
apiKey: 'test-key',
userPrompt: 'Hello',
systemPrompt: 'You are helpful',
stream: false,
);

final httpReq = AnthropicModel.instance.createRequest(req)!;

expect(
httpReq.headers?.any(
(h) => h.name == 'anthropic-version' && h.value == '2023-06-01',
),
isTrue,
);
});

test('should place system prompt as top-level field, not inside messages',
() {
const req = AIRequestModel(
modelApiProvider: ModelAPIProvider.anthropic,
url: kAnthropicUrl,
model: 'claude-3-5-sonnet-latest',
apiKey: 'test-key',
userPrompt: 'Hello',
systemPrompt: 'You are a helpful assistant',
stream: false,
);

final httpReq = AnthropicModel.instance.createRequest(req)!;
final body = jsonDecode(httpReq.body!) as Map<String, dynamic>;

// system prompt must be a top-level key
expect(body['system'], equals('You are a helpful assistant'));

// messages must contain only the user role, never system
final messages = body['messages'] as List;
expect(messages, hasLength(1));
expect(messages[0]['role'], equals('user'));
expect(
messages.any((m) => m['role'] == 'system'),
isFalse,
reason: 'Anthropic API does not support role:system inside messages',
);
});

test('should omit system field when systemPrompt is empty', () {
const req = AIRequestModel(
modelApiProvider: ModelAPIProvider.anthropic,
url: kAnthropicUrl,
model: 'claude-3-5-sonnet-latest',
userPrompt: 'Hello',
systemPrompt: '',
stream: false,
);

final httpReq = AnthropicModel.instance.createRequest(req)!;
final body = jsonDecode(httpReq.body!) as Map<String, dynamic>;

expect(body.containsKey('system'), isFalse);
});

test('should use "Generate" as fallback when userPrompt is empty', () {
const req = AIRequestModel(
modelApiProvider: ModelAPIProvider.anthropic,
url: kAnthropicUrl,
model: 'claude-3-5-sonnet-latest',
userPrompt: '',
systemPrompt: 'sys',
stream: false,
);

final httpReq = AnthropicModel.instance.createRequest(req)!;
final body = jsonDecode(httpReq.body!) as Map<String, dynamic>;
final messages = body['messages'] as List;

expect(messages[0]['content'], equals('Generate'));
});

test('should include stream:true in body when streaming is enabled', () {
const req = AIRequestModel(
modelApiProvider: ModelAPIProvider.anthropic,
url: kAnthropicUrl,
model: 'claude-3-5-sonnet-latest',
apiKey: 'test-key',
userPrompt: 'Hello',
systemPrompt: 'Sys',
stream: true,
);

final httpReq = AnthropicModel.instance.createRequest(req)!;
final body = jsonDecode(httpReq.body!) as Map<String, dynamic>;

expect(body['stream'], isTrue);
});

test('should not include stream key when streaming is disabled', () {
const req = AIRequestModel(
modelApiProvider: ModelAPIProvider.anthropic,
url: kAnthropicUrl,
model: 'claude-3-5-sonnet-latest',
userPrompt: 'Hello',
systemPrompt: 'Sys',
stream: false,
);

final httpReq = AnthropicModel.instance.createRequest(req)!;
final body = jsonDecode(httpReq.body!) as Map<String, dynamic>;

expect(body.containsKey('stream'), isFalse);
});

test('should format non-streaming output correctly', () {
final response = {
'content': [
{'text': 'Hello from Claude'},
],
};

final output = AnthropicModel.instance.outputFormatter(response);
expect(output, equals('Hello from Claude'));
});

test('should return null for malformed non-streaming output', () {
final output = AnthropicModel.instance.outputFormatter({});
expect(output, isNull);
});

test('should use POST method', () {
const req = AIRequestModel(
modelApiProvider: ModelAPIProvider.anthropic,
url: kAnthropicUrl,
model: 'claude-3-5-sonnet-latest',
userPrompt: 'Hi',
systemPrompt: '',
stream: false,
);

final httpReq = AnthropicModel.instance.createRequest(req)!;
expect(httpReq.method.name, equals('post'));
});
});
}
92 changes: 92 additions & 0 deletions packages/genai/test/interface/model_providers/ollama_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:genai/interface/consts.dart';
import 'package:genai/interface/model_providers/ollama.dart';
import 'package:genai/models/ai_request_model.dart';

void main() {
group('OllamaModel', () {
test('should return default AIRequestModel with Ollama provider', () {
final defaultModel = OllamaModel.instance.defaultAIRequestModel;

expect(defaultModel.modelApiProvider, equals(ModelAPIProvider.ollama));
expect(defaultModel.url, equals(kOllamaUrl));
});

test('should only include temperature and top_p model configs by default',
() {
final defaultModel = OllamaModel.instance.defaultAIRequestModel;
final configIds = defaultModel.modelConfigs.map((c) => c.id).toList();

expect(configIds, containsAll(['temperature', 'top_p']));
expect(configIds, isNot(contains('max_tokens')));
});

test('should use OpenAI-compatible request format', () {
const req = AIRequestModel(
modelApiProvider: ModelAPIProvider.ollama,
url: kOllamaUrl,
model: 'llama3',
userPrompt: 'Hello',
systemPrompt: 'You are helpful',
stream: false,
);

final httpReq = OllamaModel.instance.createRequest(req)!;
final body = jsonDecode(httpReq.body!) as Map<String, dynamic>;

// Ollama inherits OpenAI format: system prompt in messages
expect(body.containsKey('messages'), isTrue);
final messages = body['messages'] as List;
expect(messages[0]['role'], equals('system'));
expect(messages[1]['role'], equals('user'));
});

test('should use POST method and point to Ollama URL', () {
const req = AIRequestModel(
modelApiProvider: ModelAPIProvider.ollama,
url: kOllamaUrl,
model: 'llama3',
userPrompt: 'Hi',
systemPrompt: '',
stream: false,
);

final httpReq = OllamaModel.instance.createRequest(req)!;
expect(httpReq.method.name, equals('post'));
expect(httpReq.url, equals(kOllamaUrl));
});

test('should format output correctly (OpenAI-compatible)', () {
final response = {
'choices': [
{
'message': {'content': ' Ollama response '},
},
],
};

final output = OllamaModel.instance.outputFormatter(response);
expect(output, equals('Ollama response'));
});

test('should format streaming delta output correctly', () {
final response = {
'choices': [
{
'delta': {'content': 'stream chunk'},
},
],
};

final output = OllamaModel.instance.streamOutputFormatter(response);
expect(output, equals('stream chunk'));
});

test('should return null when aiRequestModel is null', () {
final result = OllamaModel.instance.createRequest(null);
expect(result, isNull);
});
});
}
Loading