Skip to content
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
e12689f
Studio: add Gemini provider with web_search, code_execution, prompt c…
danielhanchen May 22, 2026
9598c15
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 22, 2026
fdee834
Studio: forward presence_penalty to Gemini and recover function name …
danielhanchen May 22, 2026
780d2e3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 22, 2026
e9d4187
Studio: surface Gemini code execution parts as code_execution tool ev…
danielhanchen May 22, 2026
ca5777a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 22, 2026
7eb273a
Studio: native Gemini model catalog, function-call ids, and honest ca…
danielhanchen May 22, 2026
e41404d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 22, 2026
5eefaf9
Studio: distinct tool_calls index per emitted Gemini function call
danielhanchen May 22, 2026
696728d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 22, 2026
c6724db
Studio: surface Gemini 3.5/3.1/3 + Nano Banana 2/Pro and plumb thinki…
danielhanchen May 24, 2026
9732765
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
d277f8f
Studio: tighten Gemini provider after review (PR #5720)
danielhanchen May 24, 2026
0785ceb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
74390e0
Studio: address second-pass review findings on Gemini (PR #5720)
danielhanchen May 24, 2026
80ac142
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
d2f972e
Merge remote-tracking branch 'origin/main' into pr-5720
danielhanchen May 24, 2026
0a1c42e
Studio: fix third-pass Gemini findings (PR #5720)
danielhanchen May 24, 2026
c3c477b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
973e537
Studio: Gemini 3 thinkingLevel + image-model Search grounding (PR #5720)
danielhanchen May 24, 2026
0c1ee49
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
9372756
Studio: attach Gemini code_execution inline images to the code card (…
danielhanchen May 24, 2026
0e79d73
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
57a98bd
Studio: fix fourth-pass Gemini findings (PR #5720)
danielhanchen May 24, 2026
be2744e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
e853c10
Studio: fix fifth-pass Gemini findings (PR #5720)
danielhanchen May 24, 2026
fa22e5d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
c052941
Studio: fix sixth-pass Gemini findings (PR #5720)
danielhanchen May 24, 2026
616b43c
Studio: round-trip Gemini tool_calls and tool results (PR #5720)
danielhanchen May 24, 2026
089a63a
Studio: replay Gemini code_execution and image native parts on histor…
danielhanchen May 24, 2026
fdaf772
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
0597721
Studio: complete Gemini built-in tool replay round-trip (PR #5720)
danielhanchen May 24, 2026
3cfff64
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
92d4bb9
Studio: round 8 Gemini follow-ups (PR #5720)
danielhanchen May 24, 2026
c60b49c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
cff0785
Studio: round 9 Gemini follow-ups (PR #5720)
danielhanchen May 24, 2026
c09dffe
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
b333f1c
Studio: round 10 Gemini follow-ups (PR #5720)
danielhanchen May 24, 2026
2321731
Studio: round 11 Gemini follow-ups (PR #5720)
danielhanchen May 24, 2026
bc994ad
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
d42d907
Studio: round 12 Gemini follow-ups (PR #5720)
danielhanchen May 24, 2026
2f8eafe
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
0e0fbca
Studio: round 13 Gemini/Responses follow-ups (PR #5720)
danielhanchen May 24, 2026
45f1a67
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
5fc3734
Studio: round 14 Gemini follow-ups (PR #5720)
danielhanchen May 24, 2026
f0e4d94
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
ca7e5a5
Studio: SSRF harden Gemini remote image fetch (PR #5720)
danielhanchen May 24, 2026
77cebcd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
63143bb
Studio: round 16 Gemini follow-ups (PR #5720)
danielhanchen May 24, 2026
2df4892
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
d33ee37
Studio: round 17 Gemini follow-ups (PR #5720)
danielhanchen May 24, 2026
2d15064
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
83f5f74
Studio: round 18 Gemini follow-ups (PR #5720)
danielhanchen May 25, 2026
7326c29
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2026
e1e6a42
Studio: round 19 Gemini follow-ups (PR #5720)
danielhanchen May 25, 2026
2507849
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2026
6ed67c3
Studio: round 20 Gemini follow-ups (PR #5720)
danielhanchen May 25, 2026
0cd41cd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2026
fe1c5c1
Apply forced-function tool_choice gate to Anthropic, OpenRouter, Kimi
danielhanchen May 25, 2026
68fd4d6
Merge remote-tracking branch 'origin/main' into feat/gemini-provider
danielhanchen May 25, 2026
1890d85
Strip Gemini-only synthetic tool history on local-GGUF dispatch
danielhanchen May 25, 2026
38d5daf
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2026
9d91db0
Disable Search/Code composer pills for Gemini image-tier models
danielhanchen May 25, 2026
de04698
Apply forced-function tool_choice gate to OpenAI Responses path
danielhanchen May 25, 2026
f8d4edb
Round 24 P1s: SSRF shared-address gap + extra_content text-only leak …
danielhanchen May 25, 2026
235b7ee
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2026
41eccb5
Round 25 P1s: skip synthetic server-tool replay + inline $ref/$defs i…
danielhanchen May 25, 2026
31c9711
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2026
d6d7fa0
Round 26 P1s: drop orphan Gemini functionResponse + Anthropic /messag…
danielhanchen May 25, 2026
d0cf4b7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2026
622c409
Round 27 P1s: native_part location fallback + Gemini image request bu…
danielhanchen May 26, 2026
22dcce8
Merge main into feat/gemini-provider (resolve 5 file conflicts)
danielhanchen May 26, 2026
24bee60
Fix TS build errors from main merge: restore imageParts + refusal ret…
danielhanchen May 26, 2026
0e73786
Tighten verbose comments in external_provider.py + chat-adapter.ts
danielhanchen May 27, 2026
b19b662
Merge branch 'main' into feat/gemini-provider
danielhanchen May 27, 2026
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
1,858 changes: 1,849 additions & 9 deletions studio/backend/core/inference/external_provider.py

Large diffs are not rendered by default.

69 changes: 59 additions & 10 deletions studio/backend/core/inference/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,77 @@
},
"gemini": {
"display_name": "Google Gemini",
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai",
# Curated lineup — Google's /v1beta/openai/models returns dozens
# of historical / experimental / embedding ids. Cap to the current
# 3.x family plus the rolling `*-latest` aliases.
# Native Gemini REST endpoint -- the Gemini API does NOT speak
# OpenAI Chat Completions on this base. Requests/responses are
# translated in `_stream_gemini` in external_provider.py.
# API reference: https://ai.google.dev/gemini-api/docs
"base_url": "https://generativelanguage.googleapis.com/v1beta",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse Gemini native model catalog before switching base URL

Changing Gemini to the native https://generativelanguage.googleapis.com/v1beta endpoint routes /models to Gemini’s native schema, but our model discovery path still only consumes OpenAI-style data[].id in ExternalProviderClient.list_models(). That leaves Gemini model fetches empty in /api/providers/models (and provider test counts at 0), so users lose dynamic model discovery and rely only on hardcoded defaults. Add native parsing (models[] with name/baseModelId) or mark Gemini as curated-only to avoid silently empty catalogs.

Useful? React with 👍 / 👎.

# Curated lineup -- the live ListModels response returns dozens
# of historical / experimental / embedding ids. Cap to the
# current chat-capable Gemini families (3.5 / 3.1 / 3 Flash /
# 2.5) plus the Nano Banana image trio and the rolling
# `*-latest` aliases. Excluded on purpose:
# - `gemini-2.0-flash*` (Google retired 2026-06-01; 404 on use)
# - `gemini-3-pro-preview` (shut down 2026-03-09; auto-redirects
# to `gemini-3.1-pro-preview` per Google's deprecation notice,
# so we surface 3.1 directly and skip the redirect).
# The allowlist below blocks the retired ids from re-appearing
# via the live ListModels fetch. Verified against the live
# `/v1beta/models` catalog 2026-05-24.
"default_models": [
"gemini-3.1-pro-preview",
"gemini-3.5-flash",
"gemini-3.1-flash-lite",
"gemini-3-flash-preview",
"gemini-pro-latest",
"gemini-flash-latest",
"gemini-flash-lite-latest",
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
"gemini-3-pro-image-preview",
"gemini-3.1-flash-image-preview",
"gemini-2.5-flash-image",
],
"supports_streaming": True,
"supports_vision": True,
"supports_tool_calling": True,
"auth_header": "Authorization",
"auth_prefix": "Bearer ",
"notes": "OpenAI-compatible endpoint. API key from https://aistudio.google.com/apikey.",
# The native API takes the API key on the `x-goog-api-key`
# header. An empty `auth_prefix` ensures we send the bare key.
"auth_header": "x-goog-api-key",
"auth_prefix": "",
"openai_compatible": False,
"notes": (
"Native Gemini API. Translation lives in _stream_gemini. "
"API key from https://aistudio.google.com/apikey. "
"See https://ai.google.dev/gemini-api/docs for endpoint shapes."
),
# Even after the regex match, drop ids that Google still
# returns from ListModels but routes via implicit redirect.
# gemini-3-pro-preview was shut down 2026-03-09 and is
# auto-aliased to gemini-3.1-pro-preview; we surface the
# canonical id only so users do not see two cards for the
# same underlying model.
"model_id_deny_exact": ("gemini-3-pro-preview",),
# Matches the chat-capable 3.5 / 3.1 / 3 / 2.5 families plus the
# rolling *-latest aliases (which Google rolls forward as new
# generations ship). Image-tier ids (`-image`, `-image-preview`,
# `nano-banana-pro-preview`) flow through the Nano Banana
# `responseModalities` path in `_stream_gemini`. Retired 2.0
# ids ARE NOT in this regex on purpose -- Google's ListModels
# would otherwise re-surface them and they 404 on use.
"model_id_allowlist": re.compile(
r"^(gemini-3\.1-flash-lite|gemini-3-flash-preview|"
r"gemini-3\.1-pro-preview|gemini-pro-latest|"
r"gemini-flash-latest|gemini-flash-lite-latest)$"
r"^("
r"gemini-3\.5-(?:flash|pro)(?:-preview)?|"
r"gemini-3\.1-(?:flash|pro|flash-lite)(?:-preview)?(?:-customtools)?|"
r"gemini-3\.1-flash-image-preview|"
r"gemini-3-(?:flash|pro)(?:-preview)?|"
r"gemini-3-pro-image-preview|"
r"nano-banana-pro-preview|"
r"gemini-2\.5-pro|gemini-2\.5-flash|gemini-2\.5-flash-lite|"
r"gemini-2\.5-flash-image|"
r"gemini-pro-latest|gemini-flash-latest|gemini-flash-lite-latest"
r")$"
),
},
"deepseek": {
Expand Down
45 changes: 39 additions & 6 deletions studio/backend/models/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,14 @@ class ChatMessage(BaseModel):
None,
description = "OpenAI tool-result messages: name of the tool whose result this is.",
)
extra_content: Optional[dict] = Field(
None,
description = (
"Provider-specific extra fields the translator may read. "
"Gemini reads `extra_content.google.thought_signature` "
"from assistant messages to replay text-part signatures."
),
)

@model_validator(mode = "after")
def _validate_role_shape(self) -> "ChatMessage":
Expand Down Expand Up @@ -712,17 +720,42 @@ class ChatCompletionRequest(BaseModel):
None,
description = "[x-unsloth] Override base URL for the external provider.",
)
enable_prompt_caching: Optional[bool] = Field(
enable_prompt_caching: Optional[Union[bool, str]] = Field(
None,
description = (
"[x-unsloth] Opt in to provider-side prompt caching. On Anthropic, "
"attaches cache_control={type:ephemeral} to the system block so the "
"static prefix is reused across turns. On OpenAI cloud, caching is "
"automatic for prompts >=1024 tokens and this flag is informational. "
"Ignored for every other provider (mistral, gemini, kimi, openrouter, "
"vllm, local, etc.). Treated as enabled when omitted."
"boolean true attaches cache_control={type:ephemeral} to the system "
"block so the static prefix is reused across turns. On OpenAI cloud, "
"caching is automatic for prompts >=1024 tokens and the boolean is "
"informational. On Gemini, pass a string cache resource name such "
"as `cachedContents/abc123` to attach `cachedContent` on the native "
"request (boolean true is a no-op on Gemini because creating the "
"cache requires a separate POST /cachedContents call). Ignored for "
"every other provider. Treated as enabled when omitted."
),
)

@field_validator("enable_prompt_caching", mode = "before")
@classmethod
def _coerce_enable_prompt_caching(cls, value: Any) -> Any:
"""Preserve the pre-PR coercion: the field used to be Optional[bool],
so callers historically sent JSON strings `"true"` / `"false"` and
Pydantic v1 coerced them. Widening to Optional[Union[bool, str]] for
Gemini cache resource names lets `"false"` slip through as a truthy
string. Coerce the canonical bool literals back so explicit opt-outs
stay opt-out."""
if isinstance(value, str):
lowered = value.strip().lower()
# Match Pydantic v1's BooleanField coercion table (yes/y/on/t/1
# and no/n/off/f/0) so opt-outs that used to parse still parse.
# Anything else is preserved as a string for Gemini's
# cachedContent resource path.
if lowered in ("true", "t", "1", "yes", "y", "on"):
return True
if lowered in ("false", "f", "0", "no", "n", "off"):
return False
return value

prompt_cache_ttl: Optional[str] = Field(
None,
description = (
Expand Down
95 changes: 87 additions & 8 deletions studio/backend/routes/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,7 @@ def _build_external_messages(
messages: list,
supports_vision: bool,
provider_type: Optional[str] = None,
base_url: Optional[str] = None,
) -> list[dict]:
"""
Convert ChatMessage list to OpenAI-compatible dicts for external providers.
Expand All @@ -1717,14 +1718,61 @@ def _build_external_messages(
"""
document_provider = provider_type in _INPUT_DOCUMENT_PROVIDERS
anthropic = provider_type == "anthropic"
# `extra_content` is a Gemini-specific carrier for the assistant's
# text-part `thoughtSignature` round-trip on the native
# streamGenerateContent endpoint. Custom Gemini OpenAI-compatible
# gateways (LiteLLM etc.) route through /chat/completions where
# the field is unknown and can be rejected -- gate strictly on the
# Google-hosted Gemini base.
_native_gemini = False
if provider_type == "gemini" and base_url:
try:
from urllib.parse import urlparse as _urlparse

_host = (_urlparse(base_url).hostname or "").lower()
_native_gemini = _host == "generativelanguage.googleapis.com"
except Exception:
_native_gemini = False
emit_extra_content = _native_gemini
result = []
for msg in messages:
if isinstance(msg.content, str):
# Skip assistant messages with empty content (some providers reject them)
if msg.role == "assistant" and not msg.content.strip():
# Drop bare assistant messages with no content AND no
# tool_calls (some providers reject empty assistant turns).
# Preserve assistant turns whose only payload is tool_calls
# so multi-turn function-call loops round-trip.
if (
msg.role == "assistant"
and not msg.content.strip()
and not msg.tool_calls
):
continue
result.append({"role": msg.role, "content": msg.content})
elif isinstance(msg.content, list):
out: dict[str, Any] = {"role": msg.role, "content": msg.content}
if msg.role == "assistant" and msg.tool_calls:
out["tool_calls"] = msg.tool_calls
if msg.role == "tool":
if msg.tool_call_id:
out["tool_call_id"] = msg.tool_call_id
if msg.name:
out["name"] = msg.name
if emit_extra_content and msg.role == "assistant" and msg.extra_content:
out["extra_content"] = msg.extra_content
result.append(out)
continue
# Assistant messages with content=None but populated tool_calls
# are valid (post-tool-call assistant turn). Forward them so the
# provider helper can rebuild the functionCall part.
if msg.content is None and msg.role == "assistant" and msg.tool_calls:
_assistant_only: dict[str, Any] = {
"role": "assistant",
"content": "",
"tool_calls": msg.tool_calls,
}
if emit_extra_content and msg.extra_content:
_assistant_only["extra_content"] = msg.extra_content
result.append(_assistant_only)
continue
if isinstance(msg.content, list):
if supports_vision:
parts = []
for part in msg.content:
Expand Down Expand Up @@ -1758,7 +1806,17 @@ def _build_external_messages(
# provider would 400 on the unknown part, so
# gate by provider_type.
parts.append({"type": "compaction", "content": part.content})
result.append({"role": msg.role, "content": parts})
entry: dict[str, Any] = {"role": msg.role, "content": parts}
if msg.role == "assistant" and msg.tool_calls:
entry["tool_calls"] = msg.tool_calls
if msg.role == "tool":
if msg.tool_call_id:
entry["tool_call_id"] = msg.tool_call_id
if msg.name:
entry["name"] = msg.name
if emit_extra_content and msg.role == "assistant" and msg.extra_content:
entry["extra_content"] = msg.extra_content
result.append(entry)
else:
# Non-vision provider: strip images / documents, keep
# text, optionally keep compaction (Anthropic only --
Expand All @@ -1774,9 +1832,19 @@ def _build_external_messages(
if len(preserved) == 1 and preserved[0]["type"] == "text":
# Single text part collapses back to a string for
# providers that don't accept content arrays.
result.append({"role": msg.role, "content": preserved[0]["text"]})
entry = {"role": msg.role, "content": preserved[0]["text"]}
else:
result.append({"role": msg.role, "content": preserved})
entry = {"role": msg.role, "content": preserved}
if msg.role == "assistant" and msg.tool_calls:
entry["tool_calls"] = msg.tool_calls
if msg.role == "tool":
if msg.tool_call_id:
entry["tool_call_id"] = msg.tool_call_id
if msg.name:
entry["name"] = msg.name
if emit_extra_content and msg.role == "assistant" and msg.extra_content:
entry["extra_content"] = msg.extra_content
result.append(entry)
return result


Expand Down Expand Up @@ -1851,6 +1919,7 @@ async def _proxy_to_external_provider(
payload.messages,
_supports_vision,
provider_type = provider_type,
base_url = base_url,
)

client = ExternalProviderClient(
Expand All @@ -1859,6 +1928,14 @@ async def _proxy_to_external_provider(
api_key = api_key,
)

# `top_k` defaults to 20 in ChatCompletionRequest because the local
# inference path expects an int, but the external-provider path
# should treat "field omitted from JSON" as "use provider default"
# so callers that send only model/messages do not silently get
# different sampling than before this PR. Pydantic's
# `model_fields_set` tracks explicit-vs-default per request.
_top_k_explicit = payload.top_k if "top_k" in payload.model_fields_set else None

async def _stream():
gen = client.stream_chat_completion(
messages = chat_messages,
Expand All @@ -1867,7 +1944,7 @@ async def _stream():
top_p = payload.top_p,
max_tokens = payload.max_tokens,
presence_penalty = payload.presence_penalty,
top_k = payload.top_k,
top_k = _top_k_explicit,
enable_thinking = payload.enable_thinking,
reasoning_effort = payload.reasoning_effort,
enabled_tools = payload.enabled_tools,
Expand All @@ -1876,6 +1953,8 @@ async def _stream():
anthropic_code_exec_container_id = payload.anthropic_code_exec_container_id,
prompt_cache_ttl = payload.prompt_cache_ttl,
compaction_threshold = payload.compaction_threshold,
tools = payload.tools,
tool_choice = payload.tool_choice,
stream = payload.stream,
)
try:
Expand Down
8 changes: 7 additions & 1 deletion studio/backend/tests/test_anthropic_code_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,13 @@ async def run():
assert start["type"] == "tool_start"
assert start["tool_name"] == "code_execution"
assert start["tool_call_id"] == "srvtoolu_1"
assert start["arguments"] == {"kind": "bash", "command": "ls -la"}
# `_server_tool: True` marks this as a provider-side synthetic
# tool card for the frontend's history serializer.
assert start["arguments"] == {
"kind": "bash",
"command": "ls -la",
"_server_tool": True,
}

assert end["type"] == "tool_end"
assert end["tool_call_id"] == "srvtoolu_1"
Expand Down
7 changes: 6 additions & 1 deletion studio/backend/tests/test_anthropic_web_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,12 @@ async def run():
assert start["type"] == "tool_start"
assert start["tool_name"] == "web_fetch"
assert start["tool_call_id"] == "srvtoolu_wf1"
assert start["arguments"] == {"url": "https://example.com/article"}
# `_server_tool: True` marks this as a provider-side synthetic
# tool card for the frontend's history serializer.
assert start["arguments"] == {
"url": "https://example.com/article",
"_server_tool": True,
}
assert end["type"] == "tool_end"
assert end["tool_call_id"] == "srvtoolu_wf1"
# The source pill uses Title / URL / snippet as parseSourcesFromResult expects.
Expand Down
Loading
Loading