Skip to content
Open
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
148 changes: 1 addition & 147 deletions .github/scripts/compare-types/configs/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ const config: PackageConfig = {
},
{
name: 'LanguageModelExpected',
reason:
'Chrome Prompt API type tied to browser-only on-device language model integration.',
reason: 'Chrome Prompt API type tied to browser-only on-device language model integration.',
},
{
name: 'LanguageModelMessage',
Expand Down Expand Up @@ -101,61 +100,6 @@ const config: PackageConfig = {
reason:
'Chrome Prompt API prompt options type used by browser-only on-device language model integration.',
},
{
name: 'AnyOfSchema',
reason:
'RN Firebase schema-builder does not currently expose the `anyOf` helper class, so union-schema composition is not part of the public RN AI API.',
},
{
name: 'LiveServerGoingAwayNotice',
reason:
'RN Firebase live sessions do not currently surface the server `goingAwayNotice` message type in the public API.',
},
{
name: 'ObjectSchemaRequest',
reason:
'RN Firebase exposes `ObjectSchemaInterface` for schema helper typing, but does not separately export the raw request-shape `ObjectSchemaRequest` type.',
},
{
name: 'SingleRequestOptions',
reason:
'RN Firebase does not currently expose per-call request overrides such as `AbortSignal`; requests are configured via model-level `RequestOptions` only.',
},
{
name: 'ChatSessionBase',
reason:
'Base class used by the firebase-js-sdk template chat implementation. RN Firebase exposes its concrete chat session surface instead.',
},
{
name: 'StartTemplateChatParams',
reason:
'Template chat startup parameters are part of the firebase-js-sdk template chat API, which RN Firebase does not currently expose.',
},
{
name: 'TemplateChatSession',
reason:
'Template chat sessions are not currently part of the RN Firebase public AI API.',
},
{
name: 'TemplateFunctionDeclaration',
reason:
'Template function declaration helpers are part of firebase-js-sdk template tooling that RN Firebase does not currently expose.',
},
{
name: 'TemplateFunctionDeclarationsTool',
reason:
'Template function declaration tools are part of firebase-js-sdk template tooling that RN Firebase does not currently expose.',
},
{
name: 'TemplateTool',
reason:
'Template tool unions are part of firebase-js-sdk template tooling that RN Firebase does not currently expose.',
},
{
name: 'ThinkingLevel',
reason:
'RN Firebase supports thinking budgets but does not currently expose the JS SDK `ThinkingLevel` preset constants/type.',
},
],
extraInRN: [
{
Expand Down Expand Up @@ -210,96 +154,6 @@ const config: PackageConfig = {
reason:
'Both packages expose the same URL retrieval status constants, but the generated declaration text differs (`string`-valued object in JS SDK vs readonly literal constants in RN).',
},
{
name: 'ChatSession',
reason:
'RN Firebase chat sessions do not currently accept per-call `SingleRequestOptions`, so `sendMessage` and `sendMessageStream` expose fewer parameters.',
},
{
name: 'FunctionDeclaration',
reason:
'RN Firebase function declarations accept `ObjectSchemaInterface` only and do not expose the JS SDK `functionReference` auto-calling hook.',
},
{
name: 'FunctionResponse',
reason:
'RN Firebase function responses omit the optional `parts` field from the JS SDK declaration and only expose the structured response payload.',
},
{
name: 'GenerationConfig',
reason:
'RN Firebase does not currently expose the JS SDK `responseJsonSchema` generation config field.',
},
{
name: 'GenerativeModel',
reason:
'RN Firebase generative model methods do not currently accept per-call `SingleRequestOptions`, so request overrides are limited to model-level `RequestOptions`.',
},
{
name: 'ImagenModel',
reason:
'RN Firebase Imagen model requests do not currently accept per-call `SingleRequestOptions`, so request overrides are limited to model-level `RequestOptions`.',
},
{
name: 'LiveResponseType',
reason:
'RN Firebase live response typing omits `GOING_AWAY_NOTICE` because `LiveServerGoingAwayNotice` is not currently surfaced in the public API.',
},
{
name: 'LiveSession',
reason:
'RN Firebase live sessions do not currently expose `LiveServerGoingAwayNotice` from `receive()`, so the response union is smaller than the JS SDK.',
},
{
name: 'RequestOptions',
reason:
'RN Firebase does not currently expose `maxSequentalFunctionCalls`, so its request options are limited to timeout and base URL.',
},
{
name: 'Schema',
reason:
'RN Firebase schema-builder requires an explicit `type` and does not expose the JS SDK `anyOf` helper, so the public schema shape differs.',
},
{
name: 'SchemaInterface',
reason:
'RN Firebase schema interfaces require an explicit `type`, whereas the JS SDK declaration leaves `type` optional in the base interface.',
},
{
name: 'SchemaRequest',
reason:
'RN Firebase request-shaped schemas require an explicit `type`, whereas the JS SDK declaration leaves `type` optional.',
},
{
name: 'SchemaShared',
reason:
'RN Firebase shared schema typing omits the JS SDK `anyOf` property because `AnyOfSchema` is not currently part of the public RN API.',
},
{
name: 'TemplateGenerativeModel',
reason:
'RN Firebase template generative model methods do not currently accept per-call `SingleRequestOptions`, so request overrides are limited to model-level `RequestOptions`.',
},
{
name: 'TemplateImagenModel',
reason:
'RN Firebase template Imagen model methods do not currently accept per-call `SingleRequestOptions`, so request overrides are limited to model-level `RequestOptions`.',
},
{
name: 'ThinkingConfig',
reason:
'RN Firebase thinking config supports `thinkingBudget` and `includeThoughts`, but does not currently expose the JS SDK `thinkingLevel` preset field.',
},
{
name: 'TypedSchema',
reason:
'RN Firebase typed schema unions do not currently include `AnyOfSchema`, so the exported union is smaller than the JS SDK version.',
},
{
name: 'UsageMetadata',
reason:
'RN Firebase usage metadata does not currently surface tool-use and cache token accounting fields that are present in the JS SDK declaration.',
},
],
};

Expand Down
155 changes: 154 additions & 1 deletion packages/ai/__tests__/chat-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { describe, expect, it, afterEach, jest } from '@jest/globals';

import * as generateContentMethods from '../lib/methods/generate-content';
import { GenerateContentStreamResult } from '../lib/types';
import { EnhancedGenerateContentResponse, GenerateContentStreamResult } from '../lib/types';
import { ChatSession } from '../lib/methods/chat-session';
import { ApiSettings } from '../lib/types/internal';
import { RequestOptions } from '../lib/types/requests';
Expand All @@ -35,6 +35,15 @@ const requestOptions: RequestOptions = {
timeout: 1000,
};

function streamResult(response: EnhancedGenerateContentResponse): GenerateContentStreamResult {
return {
stream: (async function* () {
yield response;
})(),
response: Promise.resolve(response),
};
}

describe('ChatSession', () => {
afterEach(() => {
jest.restoreAllMocks();
Expand All @@ -57,6 +66,29 @@ describe('ChatSession', () => {
requestOptions,
);
});

it('merges per-call request options over session request options', async () => {
const controller = new AbortController();
const generateContentStub = jest
.spyOn(generateContentMethods, 'generateContent')
.mockResolvedValue({ response: { candidates: [] } } as any);
const chatSession = new ChatSession(fakeApiSettings, 'a-model', {}, requestOptions);

await chatSession.sendMessage('hello', {
timeout: 2000,
signal: controller.signal,
});

expect(generateContentStub).toHaveBeenCalledWith(
fakeApiSettings,
'a-model',
expect.anything(),
{
timeout: 2000,
signal: controller.signal,
},
);
});
});

describe('sendMessageStream()', () => {
Expand All @@ -81,6 +113,127 @@ describe('ChatSession', () => {
jest.useRealTimers();
});

it('merges per-call request options over session request options', async () => {
const controller = new AbortController();
const generateContentStreamStub = jest
.spyOn(generateContentMethods, 'generateContentStream')
.mockResolvedValue({
response: Promise.resolve({ candidates: [] }),
} as unknown as GenerateContentStreamResult);
const chatSession = new ChatSession(fakeApiSettings, 'a-model', {}, requestOptions);

await chatSession.sendMessageStream('hello', {
timeout: 2000,
signal: controller.signal,
});

expect(generateContentStreamStub).toHaveBeenCalledWith(
fakeApiSettings,
'a-model',
expect.anything(),
{
timeout: 2000,
signal: controller.signal,
},
);
});

it('automatically calls functionReference from stream function calls', async () => {
const getWeather = jest.fn<(args: object) => object>().mockReturnValue({ temperature: 72 });
const functionCallResponse = {
candidates: [
{
index: 0,
content: {
role: 'model',
parts: [
{
functionCall: {
name: 'getWeather',
args: { city: 'London' },
},
},
],
},
},
],
functionCalls: () => [{ name: 'getWeather', args: { city: 'London' } }],
} as EnhancedGenerateContentResponse;
const finalResponse = {
candidates: [
{
index: 0,
content: {
role: 'model',
parts: [{ text: 'It is 72 degrees.' }],
},
},
],
functionCalls: () => undefined,
} as EnhancedGenerateContentResponse;
const generateContentStreamStub = jest
.spyOn(generateContentMethods, 'generateContentStream')
.mockResolvedValueOnce(streamResult(functionCallResponse))
.mockResolvedValueOnce(streamResult(finalResponse));
const chatSession = new ChatSession(
fakeApiSettings,
'a-model',
{
tools: [
{
functionDeclarations: [
{
name: 'getWeather',
description: 'Gets weather for a city.',
functionReference: getWeather,
},
],
},
],
},
requestOptions,
);

const result = await chatSession.sendMessageStream('weather in London');
await result.response;
const history = await chatSession.getHistory();

expect(getWeather).toHaveBeenCalledWith({ city: 'London' });
expect(generateContentStreamStub).toHaveBeenCalledTimes(2);
expect(history).toEqual([
{
role: 'user',
parts: [{ text: 'weather in London' }],
},
{
role: 'model',
parts: [
{
functionCall: {
name: 'getWeather',
args: { city: 'London' },
},
},
],
},
{
role: 'function',
parts: [
{
functionResponse: {
name: 'getWeather',
response: { temperature: 72 },
},
},
],
},
{
role: 'model',
parts: [{ text: 'It is 72 degrees.' }],
},
]);
});

it('downstream sendPromise errors should log but not throw', async () => {
const consoleStub = jest.spyOn(console, 'error').mockImplementation(() => {});
// make response undefined so that response.candidates errors
Expand Down
Loading
Loading