Skip to content

VinF Hybrid Inference: introduce Chrome Adapter class (rebased) #8932

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
556c348
Define HybridParams
erikeldridge Mar 24, 2025
272ff16
Copy over most types from @types package
erikeldridge Mar 24, 2025
aa9c4f0
Document HybridParams and InferenceMode
erikeldridge Mar 24, 2025
12e07c0
Trim unused AI types
erikeldridge Mar 25, 2025
f100306
Assert HybridParams sets the model name
erikeldridge Mar 25, 2025
15c4e45
Use dom-chromium-ai package directly
erikeldridge Mar 26, 2025
9786718
Use multi-line JSDoc comments for enum
erikeldridge Mar 26, 2025
c12b529
Use the existing name of the model params input
erikeldridge Mar 27, 2025
bbc83d9
Run docgen:all
erikeldridge Mar 27, 2025
33db72a
Use in-cloud phrasing
erikeldridge Mar 27, 2025
4f22b15
Parameterize default in-cloud model name
erikeldridge Mar 28, 2025
6df8527
Use type for inference mode and update docs
erikeldridge Apr 2, 2025
7a6c8e9
Run yarn format
erikeldridge Apr 3, 2025
eac3d4c
rebased and updated deps
hsubox76 Apr 14, 2025
c6a17b0
revert deps
hsubox76 Apr 14, 2025
6ccdc0e
Remove erroneous type dep
erikeldridge Apr 15, 2025
b07bdac
Vaihi add langmodel types. (#8927)
gsiddh Apr 15, 2025
5302797
Took dependency on new type
gsiddh Apr 15, 2025
776dd0d
updating to new type
gsiddh Apr 16, 2025
fe18abb
docgen
gsiddh Apr 16, 2025
31a46a1
post yarn format
gsiddh Apr 16, 2025
d0fb0a5
Define ChromeAdapter class
erikeldridge Mar 25, 2025
77d0a70
Use type for inference mode and update docs
erikeldridge Apr 2, 2025
f31e32a
Remove stray ai.ts
erikeldridge Apr 3, 2025
a7d4827
Run yarn format
erikeldridge Apr 3, 2025
8975891
Update content generator to emulate Vertex response
erikeldridge Apr 8, 2025
ccf1d2b
Update tests to pass after refactor to Response
erikeldridge Apr 8, 2025
bb75d91
Inject LanguageModel provider
erikeldridge Apr 16, 2025
fadda57
Run gendoc
erikeldridge Apr 16, 2025
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
19 changes: 16 additions & 3 deletions common/api-review/vertexai.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export class BooleanSchema extends Schema {
// @public
export class ChatSession {
// Warning: (ae-forgotten-export) The symbol "ApiSettings" needs to be exported by the entry point index.d.ts
constructor(apiSettings: ApiSettings, model: string, params?: StartChatParams | undefined, requestOptions?: RequestOptions | undefined);
// Warning: (ae-forgotten-export) The symbol "ChromeAdapter" needs to be exported by the entry point index.d.ts
constructor(apiSettings: ApiSettings, model: string, chromeAdapter: ChromeAdapter, params?: StartChatParams | undefined, requestOptions?: RequestOptions | undefined);
getHistory(): Promise<Content[]>;
// (undocumented)
model: string;
Expand Down Expand Up @@ -324,8 +325,9 @@ export interface GenerativeContentBlob {

// @public
export class GenerativeModel extends VertexAIModel {
constructor(vertexAI: VertexAI, modelParams: ModelParams, requestOptions?: RequestOptions);
constructor(vertexAI: VertexAI, modelParams: ModelParams, chromeAdapter: ChromeAdapter, requestOptions?: RequestOptions);
countTokens(request: CountTokensRequest | string | Array<string | Part>): Promise<CountTokensResponse>;
static DEFAULT_HYBRID_IN_CLOUD_MODEL: string;
generateContent(request: GenerateContentRequest | string | Array<string | Part>): Promise<GenerateContentResult>;
generateContentStream(request: GenerateContentRequest | string | Array<string | Part>): Promise<GenerateContentStreamResult>;
// (undocumented)
Expand All @@ -344,7 +346,7 @@ export class GenerativeModel extends VertexAIModel {
}

// @public
export function getGenerativeModel(vertexAI: VertexAI, modelParams: ModelParams, requestOptions?: RequestOptions): GenerativeModel;
export function getGenerativeModel(vertexAI: VertexAI, modelParams: ModelParams | HybridParams, requestOptions?: RequestOptions): GenerativeModel;

// @beta
export function getImagenModel(vertexAI: VertexAI, modelParams: ImagenModelParams, requestOptions?: RequestOptions): ImagenModel;
Expand Down Expand Up @@ -416,6 +418,14 @@ export enum HarmSeverity {
HARM_SEVERITY_NEGLIGIBLE = "HARM_SEVERITY_NEGLIGIBLE"
}

// @public
export interface HybridParams {
inCloudParams?: ModelParams;
mode: InferenceMode;
// Warning: (ae-forgotten-export) The symbol "LanguageModelCreateOptions" needs to be exported by the entry point index.d.ts
onDeviceParams?: LanguageModelCreateOptions;
}

// @beta
export enum ImagenAspectRatio {
LANDSCAPE_16x9 = "16:9",
Expand Down Expand Up @@ -500,6 +510,9 @@ export interface ImagenSafetySettings {
safetyFilterLevel?: ImagenSafetyFilterLevel;
}

// @public
export type InferenceMode = 'prefer_on_device' | 'only_on_device' | 'only_in_cloud';

// @public
export interface InlineDataPart {
// (undocumented)
Expand Down
2 changes: 2 additions & 0 deletions docs-devsite/_toc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,8 @@ toc:
path: /docs/reference/js/vertexai.groundingattribution.md
- title: GroundingMetadata
path: /docs/reference/js/vertexai.groundingmetadata.md
- title: HybridParams
path: /docs/reference/js/vertexai.hybridparams.md
- title: ImagenGCSImage
path: /docs/reference/js/vertexai.imagengcsimage.md
- title: ImagenGenerationConfig
Expand Down
5 changes: 3 additions & 2 deletions docs-devsite/vertexai.chatsession.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export declare class ChatSession

| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(apiSettings, model, params, requestOptions)](./vertexai.chatsession.md#chatsessionconstructor) | | Constructs a new instance of the <code>ChatSession</code> class |
| [(constructor)(apiSettings, model, chromeAdapter, params, requestOptions)](./vertexai.chatsession.md#chatsessionconstructor) | | Constructs a new instance of the <code>ChatSession</code> class |

## Properties

Expand All @@ -47,7 +47,7 @@ Constructs a new instance of the `ChatSession` class
<b>Signature:</b>

```typescript
constructor(apiSettings: ApiSettings, model: string, params?: StartChatParams | undefined, requestOptions?: RequestOptions | undefined);
constructor(apiSettings: ApiSettings, model: string, chromeAdapter: ChromeAdapter, params?: StartChatParams | undefined, requestOptions?: RequestOptions | undefined);
```

#### Parameters
Expand All @@ -56,6 +56,7 @@ constructor(apiSettings: ApiSettings, model: string, params?: StartChatParams |
| --- | --- | --- |
| apiSettings | ApiSettings | |
| model | string | |
| chromeAdapter | ChromeAdapter | |
| params | [StartChatParams](./vertexai.startchatparams.md#startchatparams_interface) \| undefined | |
| requestOptions | [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) \| undefined | |

Expand Down
16 changes: 14 additions & 2 deletions docs-devsite/vertexai.generativemodel.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ export declare class GenerativeModel extends VertexAIModel

| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(vertexAI, modelParams, requestOptions)](./vertexai.generativemodel.md#generativemodelconstructor) | | Constructs a new instance of the <code>GenerativeModel</code> class |
| [(constructor)(vertexAI, modelParams, chromeAdapter, requestOptions)](./vertexai.generativemodel.md#generativemodelconstructor) | | Constructs a new instance of the <code>GenerativeModel</code> class |

## Properties

| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [DEFAULT\_HYBRID\_IN\_CLOUD\_MODEL](./vertexai.generativemodel.md#generativemodeldefault_hybrid_in_cloud_model) | <code>static</code> | string | Defines the name of the default in-cloud model to use for hybrid inference. |
| [generationConfig](./vertexai.generativemodel.md#generativemodelgenerationconfig) | | [GenerationConfig](./vertexai.generationconfig.md#generationconfig_interface) | |
| [requestOptions](./vertexai.generativemodel.md#generativemodelrequestoptions) | | [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | |
| [safetySettings](./vertexai.generativemodel.md#generativemodelsafetysettings) | | [SafetySetting](./vertexai.safetysetting.md#safetysetting_interface)<!-- -->\[\] | |
Expand All @@ -52,7 +53,7 @@ Constructs a new instance of the `GenerativeModel` class
<b>Signature:</b>

```typescript
constructor(vertexAI: VertexAI, modelParams: ModelParams, requestOptions?: RequestOptions);
constructor(vertexAI: VertexAI, modelParams: ModelParams, chromeAdapter: ChromeAdapter, requestOptions?: RequestOptions);
```

#### Parameters
Expand All @@ -61,8 +62,19 @@ constructor(vertexAI: VertexAI, modelParams: ModelParams, requestOptions?: Reque
| --- | --- | --- |
| vertexAI | [VertexAI](./vertexai.vertexai.md#vertexai_interface) | |
| modelParams | [ModelParams](./vertexai.modelparams.md#modelparams_interface) | |
| chromeAdapter | ChromeAdapter | |
| requestOptions | [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | |

## GenerativeModel.DEFAULT\_HYBRID\_IN\_CLOUD\_MODEL

Defines the name of the default in-cloud model to use for hybrid inference.

<b>Signature:</b>

```typescript
static DEFAULT_HYBRID_IN_CLOUD_MODEL: string;
```

## GenerativeModel.generationConfig

<b>Signature:</b>
Expand Down
57 changes: 57 additions & 0 deletions docs-devsite/vertexai.hybridparams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# HybridParams interface
Toggles hybrid inference.

<b>Signature:</b>

```typescript
export interface HybridParams
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [inCloudParams](./vertexai.hybridparams.md#hybridparamsincloudparams) | [ModelParams](./vertexai.modelparams.md#modelparams_interface) | Optional. Specifies advanced params for in-cloud inference. |
| [mode](./vertexai.hybridparams.md#hybridparamsmode) | [InferenceMode](./vertexai.md#inferencemode) | Specifies on-device or in-cloud inference. Defaults to prefer on-device. |
| [onDeviceParams](./vertexai.hybridparams.md#hybridparamsondeviceparams) | LanguageModelCreateOptions | Optional. Specifies advanced params for on-device inference. |

## HybridParams.inCloudParams

Optional. Specifies advanced params for in-cloud inference.

<b>Signature:</b>

```typescript
inCloudParams?: ModelParams;
```

## HybridParams.mode

Specifies on-device or in-cloud inference. Defaults to prefer on-device.

<b>Signature:</b>

```typescript
mode: InferenceMode;
```

## HybridParams.onDeviceParams

Optional. Specifies advanced params for on-device inference.

<b>Signature:</b>

```typescript
onDeviceParams?: LanguageModelCreateOptions;
```
24 changes: 18 additions & 6 deletions docs-devsite/vertexai.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The Vertex AI in Firebase Web SDK.
| <b>function(app, ...)</b> |
| [getVertexAI(app, options)](./vertexai.md#getvertexai_04094cf) | Returns a [VertexAI](./vertexai.vertexai.md#vertexai_interface) instance for the given app. |
| <b>function(vertexAI, ...)</b> |
| [getGenerativeModel(vertexAI, modelParams, requestOptions)](./vertexai.md#getgenerativemodel_e3037c9) | Returns a [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. |
| [getGenerativeModel(vertexAI, modelParams, requestOptions)](./vertexai.md#getgenerativemodel_8dbc150) | Returns a [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. |
| [getImagenModel(vertexAI, modelParams, requestOptions)](./vertexai.md#getimagenmodel_812c375) | <b><i>(Public Preview)</i></b> Returns an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen.<!-- -->Only Imagen 3 models (named <code>imagen-3.0-*</code>) are supported. |

## Classes
Expand Down Expand Up @@ -91,6 +91,7 @@ The Vertex AI in Firebase Web SDK.
| [GenerativeContentBlob](./vertexai.generativecontentblob.md#generativecontentblob_interface) | Interface for sending an image. |
| [GroundingAttribution](./vertexai.groundingattribution.md#groundingattribution_interface) | |
| [GroundingMetadata](./vertexai.groundingmetadata.md#groundingmetadata_interface) | Metadata returned to client when grounding is enabled. |
| [HybridParams](./vertexai.hybridparams.md#hybridparams_interface) | Toggles hybrid inference. |
| [ImagenGCSImage](./vertexai.imagengcsimage.md#imagengcsimage_interface) | An image generated by Imagen, stored in a Cloud Storage for Firebase bucket.<!-- -->This feature is not available yet. |
| [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface) | <b><i>(Public Preview)</i></b> Configuration options for generating images with Imagen.<!-- -->See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images-imagen) for more details. |
| [ImagenGenerationResponse](./vertexai.imagengenerationresponse.md#imagengenerationresponse_interface) | <b><i>(Public Preview)</i></b> The response from a request to generate images with Imagen. |
Expand All @@ -99,10 +100,10 @@ The Vertex AI in Firebase Web SDK.
| [ImagenSafetySettings](./vertexai.imagensafetysettings.md#imagensafetysettings_interface) | <b><i>(Public Preview)</i></b> Settings for controlling the aggressiveness of filtering out sensitive content.<!-- -->See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details. |
| [InlineDataPart](./vertexai.inlinedatapart.md#inlinedatapart_interface) | Content part interface if the part represents an image. |
| [ModalityTokenCount](./vertexai.modalitytokencount.md#modalitytokencount_interface) | Represents token counting info for a single modality. |
| [ModelParams](./vertexai.modelparams.md#modelparams_interface) | Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9)<!-- -->. |
| [ModelParams](./vertexai.modelparams.md#modelparams_interface) | Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_8dbc150)<!-- -->. |
| [ObjectSchemaInterface](./vertexai.objectschemainterface.md#objectschemainterface_interface) | Interface for [ObjectSchema](./vertexai.objectschema.md#objectschema_class) class. |
| [PromptFeedback](./vertexai.promptfeedback.md#promptfeedback_interface) | If the prompt was blocked, this will be populated with <code>blockReason</code> and the relevant <code>safetyRatings</code>. |
| [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9)<!-- -->. |
| [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_8dbc150)<!-- -->. |
| [RetrievedContextAttribution](./vertexai.retrievedcontextattribution.md#retrievedcontextattribution_interface) | |
| [SafetyRating](./vertexai.safetyrating.md#safetyrating_interface) | A safety rating associated with a [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface) |
| [SafetySetting](./vertexai.safetysetting.md#safetysetting_interface) | Safety setting that can be sent as part of request parameters. |
Expand Down Expand Up @@ -130,6 +131,7 @@ The Vertex AI in Firebase Web SDK.

| Type Alias | Description |
| --- | --- |
| [InferenceMode](./vertexai.md#inferencemode) | Determines whether inference happens on-device or in-cloud. |
| [Part](./vertexai.md#part) | Content part - includes text, image/video, or function call/response part types. |
| [Role](./vertexai.md#role) | Role is the producer of the content. |
| [Tool](./vertexai.md#tool) | Defines a tool that model can call to access external knowledge. |
Expand Down Expand Up @@ -160,22 +162,22 @@ export declare function getVertexAI(app?: FirebaseApp, options?: VertexAIOptions

## function(vertexAI, ...)

### getGenerativeModel(vertexAI, modelParams, requestOptions) {:#getgenerativemodel_e3037c9}
### getGenerativeModel(vertexAI, modelParams, requestOptions) {:#getgenerativemodel_8dbc150}

Returns a [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality.

<b>Signature:</b>

```typescript
export declare function getGenerativeModel(vertexAI: VertexAI, modelParams: ModelParams, requestOptions?: RequestOptions): GenerativeModel;
export declare function getGenerativeModel(vertexAI: VertexAI, modelParams: ModelParams | HybridParams, requestOptions?: RequestOptions): GenerativeModel;
```

#### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| vertexAI | [VertexAI](./vertexai.vertexai.md#vertexai_interface) | |
| modelParams | [ModelParams](./vertexai.modelparams.md#modelparams_interface) | |
| modelParams | [ModelParams](./vertexai.modelparams.md#modelparams_interface) \| [HybridParams](./vertexai.hybridparams.md#hybridparams_interface) | |
| requestOptions | [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | |

<b>Returns:</b>
Expand Down Expand Up @@ -223,6 +225,16 @@ Possible roles.
POSSIBLE_ROLES: readonly ["user", "model", "function", "system"]
```

## InferenceMode

Determines whether inference happens on-device or in-cloud.

<b>Signature:</b>

```typescript
export type InferenceMode = 'prefer_on_device' | 'only_on_device' | 'only_in_cloud';
```

## Part

Content part - includes text, image/video, or function call/response part types.
Expand Down
2 changes: 1 addition & 1 deletion docs-devsite/vertexai.modelparams.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# ModelParams interface
Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9)<!-- -->.
Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_8dbc150)<!-- -->.

<b>Signature:</b>

Expand Down
2 changes: 1 addition & 1 deletion docs-devsite/vertexai.requestoptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# RequestOptions interface
Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9)<!-- -->.
Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_8dbc150)<!-- -->.

<b>Signature:</b>

Expand Down
21 changes: 13 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,16 @@
"type": "git",
"url": "git+https://github.com/firebase/firebase-js-sdk.git"
},
"workspaces": [
"packages/*",
"integration/*",
"repo-scripts/*"
],
"workspaces": {
"packages": [
"packages/*",
"integration/*",
"repo-scripts/*"
],
"nohoist": [
"**/vertexai/@types/dom-chromium-ai"
]
},
"devDependencies": {
"@babel/core": "7.26.8",
"@babel/plugin-transform-modules-commonjs": "7.26.3",
Expand All @@ -80,7 +85,7 @@
"@types/long": "4.0.2",
"@types/mocha": "9.1.1",
"@types/mz": "2.7.8",
"@types/node": "18.19.83",
"@types/node": "18.19.75",
"@types/request": "2.48.12",
"@types/sinon": "9.0.11",
"@types/sinon-chai": "3.2.12",
Expand Down Expand Up @@ -139,7 +144,7 @@
"nyc": "15.1.0",
"ora": "5.4.1",
"patch-package": "7.0.2",
"playwright": "1.51.1",
"playwright": "1.50.1",
"postinstall-postinstall": "2.1.0",
"prettier": "2.8.8",
"protractor": "5.4.2",
Expand All @@ -158,7 +163,7 @@
"typedoc": "0.16.11",
"typescript": "5.5.4",
"watch": "1.0.2",
"webpack": "5.98.0",
"webpack": "5.97.1",
"yargs": "17.7.2"
}
}
7 changes: 4 additions & 3 deletions packages/vertexai/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firebase/vertexai",
"version": "1.2.1",
"version": "1.2.0",
"description": "A Firebase SDK for VertexAI",
"author": "Firebase <[email protected]> (https://firebase.google.com/)",
"engines": {
Expand Down Expand Up @@ -56,8 +56,9 @@
},
"license": "Apache-2.0",
"devDependencies": {
"@firebase/app": "0.11.4",
"@firebase/app": "0.11.3",
"@rollup/plugin-json": "6.1.0",
"@types/dom-chromium-ai": "0.0.6",
"rollup": "2.79.2",
"rollup-plugin-replace": "2.2.0",
"rollup-plugin-typescript2": "0.36.0",
Expand All @@ -78,4 +79,4 @@
],
"reportDir": "./coverage/node"
}
}
}
15 changes: 15 additions & 0 deletions packages/vertexai/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ describe('Top level API', () => {
expect(genModel).to.be.an.instanceOf(GenerativeModel);
expect(genModel.model).to.equal('publishers/google/models/my-model');
});
it('getGenerativeModel with HybridParams sets a default model', () => {
const genModel = getGenerativeModel(fakeVertexAI, {
mode: 'only_on_device'
});
expect(genModel.model).to.equal(
`publishers/google/models/${GenerativeModel.DEFAULT_HYBRID_IN_CLOUD_MODEL}`
);
});
it('getGenerativeModel with HybridParams honors a model override', () => {
const genModel = getGenerativeModel(fakeVertexAI, {
mode: 'only_in_cloud',
inCloudParams: { model: 'my-model' }
});
expect(genModel.model).to.equal('publishers/google/models/my-model');
});
it('getImagenModel throws if no model is provided', () => {
try {
getImagenModel(fakeVertexAI, {} as ImagenModelParams);
Expand Down
Loading
Loading