Commit Graph

475 Commits

Author SHA1 Message Date
Teknium 95177359f8 refactor(agent/aux): ruff SIM cleanups; compact vision custom-runtime recovery 2026-09-02 13:33:04 -07:00
Teknium 05d1136dea refactor(agent/aux): fallback reason via ordered predicate table; single loop for stale-credential re-walk 2026-09-02 13:33:01 -07:00
Teknium d8ef59a415 refactor(agent/aux): recovery-ladder rungs via one _rung() generator helper with named accept predicates 2026-09-02 13:33:01 -07:00
Teknium da388692e4 refactor(agent/aux): one _field accessor for dict/object wire payloads 2026-09-02 13:32:58 -07:00
Teknium 6320c54552 refactor(agent/aux): _to_async_client header selection via _endpoint_default_headers 2026-09-02 13:32:56 -07:00
Teknium 0d41e9bdbc refactor(agent/aux): shared pool loader; restore @_relay_auxiliary_call placement on call_llm 2026-09-02 13:32:55 -07:00
Teknium 14ec5874a5 refactor(agent/aux): collapse cancel-check readers and Codex-route model predicates 2026-09-02 13:32:53 -07:00
Teknium b40d76823f refactor(agent/aux): shared creds-pair and Nous pool entry-state helpers 2026-09-02 13:32:51 -07:00
Teknium 9001ed2365 refactor(agent/aux): shared _prepare_aux_request head for call_llm/async_call_llm 2026-09-02 13:32:49 -07:00
Teknium d46aa77cde refactor(agent/aux): extract named-custom credential resolution, Bedrock and Vertex client builders out of resolve_provider_client 2026-09-02 13:32:45 -07:00
Teknium e3d352872b refactor(agent/aux): shared chain-label aliases, context-window screen, event-loop helper 2026-09-02 13:32:41 -07:00
Teknium 575cd5f34e refactor(agent/aux): shared hook ticker and _is_specialized_aux_client predicate 2026-09-02 13:32:41 -07:00
Teknium 5dfda6a615 refactor(agent/aux): compact _fallback_destination 2026-09-02 13:32:12 -07:00
Teknium 28c3cb1561 refactor(agent/aux): shared _endpoint_default_headers, single primary-attempt closure in call_llm impls, vision tail dedupe 2026-09-02 13:32:09 -07:00
Teknium 936c8bb626 refactor(agent/aux): split _CodexCompletionsAdapter.create into request build / final-response parse; dedupe stream/client close paths 2026-09-02 13:32:07 -07:00
Teknium 312378d9b1 refactor(agent/aux): unify _read_main_{model,provider,api_key,base_url} and relay call-context builders 2026-09-02 13:32:03 -07:00
Teknium 4022e69e69 refactor(agent/aux): lift max_tokens forwarding policy and Nous dual-wire check out of _build_call_kwargs 2026-09-02 13:32:00 -07:00
Teknium dcbf263a09 refactor(agent/aux): unify same-provider retry and fallback-candidate prep across sync/async (_prepare_same_provider_retry, _plan_fallback_candidate) 2026-09-02 13:31:57 -07:00
Teknium 127a179ae5 refactor(agent/aux): share call_llm/async_call_llm route resolution (_resolve_call_client) and recovery ladder (_aux_recovery_ladder generator) across sync/async wires 2026-09-02 13:31:57 -07:00
Teknium 369ebf7532 refactor(agent/aux): resolve_provider_client — single _route() for sync/async return, dedupe named-custom OpenAI-wire build 2026-09-02 13:30:38 -07:00
Teknium b12e4a90c4 refactor(agent/aux): dedupe api-key auto-chain client build (pool vs env creds) and provider-profile header lookup 2026-09-02 13:30:15 -07:00
Teknium 7e9c8a714e refactor(agent/aux): unify Codex/Anthropic/Bedrock chat shims + async adapters into _ChatShim/_AsyncCompletionsAdapter/_AsyncAuxiliaryClientBase 2026-09-02 13:30:15 -07:00
Teknium d7c6cef154 refactor(agent/aux): drop zero-reference helpers (_resolve_single_provider, get_async_text_auxiliary_client, _is_streaming_rejected_error, get_accounting_context, unused copy import) 2026-09-02 13:30:15 -07:00
Teknium 7b8aed652f refactor(agent/aux): compact module/function docstrings and comment essays to their invariants (behavior-neutral)
Worker was killed mid-flight by the Nous credential refresh stampede.
Unreviewed, unverified. Preserved verbatim so it can be assessed.
2026-09-02 13:30:14 -07:00
kshitijk4poor c4e394cdf8 refactor(auxiliary): one predicate for the critical-path retry skip
The sync and async retry sites each re-derived the same three-clause
decision (critical task + full-budget timeout + not a no-progress fail) with
their own copy of the rationale — which is exactly how the async site drifted
in the first place. _should_skip_same_provider_retry() now owns the rule and
its carve-out next to _TIMEOUT_NO_RETRY_TASKS; both sites call it.

Behavior-preserving: same clauses, same exception object, same outer guard.
2026-09-03 01:33:00 +05:30
kshitijk4poor 8115ff897a fix(auxiliary): mirror the no-progress carve-out on the async timeout skip
f50b5bb0fa taught the sync retry site to keep the cheap same-provider retry
when a Codex stream dies inside the 60s no-progress window (zero output),
skipping straight to fallback only on a stall or hard-ceiling timeout. The
async site never got that carve-out, so after widening the skip to vision
(#97572) an async vision call on a stillborn stream would have jumped to
fallback where the sync path retries. Both sites now apply the same rule.

Adds the async twin of the vision-skip test and a no-progress-still-retries
guard for the async site.
2026-09-03 01:33:00 +05:30
xmhua 827cf6fa01 fix(auxiliary): skip same-provider retry on a vision full-budget timeout
Issue #54465 established that a same-provider retry after a full-budget
timeout costs a second whole `timeout` window before the fallback chain is
reached, doubling the user-visible stall, and that compression must not pay
it because it sits on a critical path. The guard added for that is spelled
`task == "compression"`, so vision — which sits on the interactive path —
still retries.

The cost is the same and the stall is more visible: the turn holding the
image cannot answer, and because turns are serialised the following user
messages queue behind it. Two sequential full-budget timeouts on an
unhealthy vision provider is a long stall for something the fallback chain
could have served immediately.

Replaces the string comparison at both retry sites (sync `call_llm` and
`async_call_llm`) with `_TIMEOUT_NO_RETRY_TASKS = {"compression", "vision"}`,
so the two paths cannot drift again. Behaviour is unchanged for every other
task: fast blips (a streaming-close or a 5xx) still retry, and only
full-budget timeouts on those two tasks skip straight to fallback.

Tests: vision now falls straight through to fallback with the primary tried
exactly once, and a non-critical task still gets its one same-provider
retry, so the change stays scoped. Reverting the source change fails the
vision test and leaves the scoping test green.

Not the same as #51513, which fixes five separate defects in the vision
fallback chain (capability detection, sync/async client misuse, geo-block
and RemoteProtocolError classification, and chain iteration). This is about
what happens before that chain is reached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 01:33:00 +05:30
cryptoyasenka 3265801900 fix(aux-client): evict expired auxiliary client on 401 by refreshing under the lookup cache key
On the default Nous config, call_llm acquires the auxiliary client via
_get_cached_client(resolved_model=None), so the cache key's model
element is "". On a 401, _refresh_nous_auxiliary_client rebuilt the
client but keyed the new entry on the resolved wire model (final_model,
e.g. "Hermes-4-405B"). The fresh client therefore landed under a
different key than the lookup, and the stale expired-credential client
under "" was never overwritten: every auxiliary call kept hitting the
dead client, 401ing and forcing a credential portal round-trip on each
request instead of self-healing after the first refresh.

The auto-provider dimensions had the same divergence: call_llm and
async_call_llm dropped task at both acquisition sites, and the async
path additionally dropped main_runtime at acquisition and at both of
its refresh sites, so the refreshed client shadowed the stale one under
a divergent (provider, task, model, runtime) key.

Pass the original lookup model (which may be None) into the refresh as
a separate lookup_model argument used only to build the cache key,
while the resolved model is still stored as the entry's usable model
and returned to the caller. Thread task into both acquisition sites and
main_runtime into the async acquisition and both async refresh sites,
so sync and async compute the same cache key on acquire and on refresh.
The stale client is now overwritten in place instead of lingering under
an orphaned key, preserving the per-model cache keying introduced in
on the default config.

Add end-to-end regression tests that drive the real call_llm and
async_call_llm through the real client cache; the existing 401 tests
patch _get_cached_client wholesale and so cannot observe
acquire/refresh key divergence.
2026-09-02 10:55:46 -07:00
Alexander Prendota 1131b22856 feat(providers): let a provider profile supply its own client
``create_openai_client`` was a hardcoded if-ladder: copilot-acp builds an ACP
stdio shim, gemini builds a native client, everything else gets an
``openai.OpenAI``. There was no extension point, so a provider whose wire
protocol is not OpenAI-over-HTTP could only be added by editing this function —
which is exactly why an ACP provider cannot ship outside this tree today, even
though ``providers/__init__.py`` has discovered out-of-tree profiles from
``~/.hermes/plugins/model-providers/`` and pip entry points for a while.

``ProviderProfile.create_client(**client_kwargs)`` closes that gap. It returns
``None`` by default, so every provider that wants the standard client is
unaffected and the existing ladder still runs as the fallback. copilot-acp is
migrated onto it — its hardcoded branch is gone and its profile supplies the
client in three lines, which is the same three lines an external package writes.

Resolution goes by provider name first, then by ``base_url`` prefix, so a
runtime configured only by URL still reaches its profile — matching what the
replaced ``startswith("acp://copilot")`` branch did. A profile that raises is
logged and skipped: a third-party plugin can fail to provide a client, but it
cannot take the turn down.

Also replaces the two ``isinstance`` checks in ``agent/auxiliary_client.py``
that mean "this client is complete, do not wrap it" with capability flags the
client class declares — ``HERMES_SKIP_TRANSPORT_WRAP`` and
``HERMES_SKIP_ASYNC_WRAP``, mirroring ``SUPPORTS_HERMES_TOOL_CALLS`` in
``background_review.py``. Two in-tree consumers (the ACP shim and the Gemini
native client), an out-of-tree client is covered by the same declaration, and
the hot path no longer imports those modules just to type-test.

Co-Authored-By: Junie <junie@jetbrains.com>
2026-09-02 09:57:39 -07:00
Teknium 30c9d40974 fix(compression): stop the Codex and Anthropic aux summary streams at the host deadline too (#99692)
PR #99779 gave the streamed chat.completions consumer the host's absolute
compression deadline. The two wires that consume their streams internally
still ran on their own, always-larger budgets after the host gave up:

- Codex Responses: clamp the re-armable watchdog's hard ceiling to the
  published host deadline, so a live (re-arming) stream is severed the
  instant the host stops waiting instead of at max(600s, 4x timeout).
- Anthropic Messages: the per-event hook now raises at the host deadline
  and on an explicit hard cancel; create_anthropic_message lets that
  TimeoutError abandon the stream (the with-block closes it) instead of
  swallowing it as a callback failure.

Sabotage-verified: without the Codex clamp the new deadline test hangs past
its 25s harness cutoff; without the Anthropic hook the three Anthropic tests
fail.
2026-09-01 23:56:06 -07:00
joaomarcos 904e5bb572 fix(compression): stop the summary stream at the host's own deadline
CompressionCommitFence.set_total_ceiling_seconds documents its deadline as
"shared by the host and worker", but only the host ever read it. The worker's
streamed summary bounds itself with _aux_stream_total_ceiling() instead —
max(600, 4 * aux_timeout) — which is >= the host's total ceiling for every
configured timeout AND starts counting later (after pool admission,
_serialize_for_summary, prompt build and TTFT). A stream that outlives its
abandoned host is therefore not an edge case; it is the guaranteed outcome of
every total-ceiling timeout.

8207862212 closed the first half: a cancelled fence now releases the
compression owner, freeing its pool slot and session lease. Its own comment
leaves the second half open — the isolated provider daemon that holds the
socket keeps streaming "until the auxiliary stream's longer absolute ceiling
expires". With the #99692 reporter's auxiliary.compression.timeout: 600 that
is 2400s of an orphaned ~500K-token summary the fence is already guaranteed to
refuse, and because the session never shrank, every following turn stacks a
fresh orphan on top of the last.

Publish the fence's deadline as an absolute monotonic instant
(CompressionCommitFence.deadline_monotonic) and give the auxiliary layer the
return leg it was missing: aux_stream_deadline() installs it thread-locally,
_ChatStreamAccumulator.feed() stops the stream once it passes, and
_run_protected_sync_provider_call propagates it onto the provider daemon
(thread-locals do not cross that boundary, so an owner-thread-only install
would be inert on exactly the path large-session compression takes).

Absolute, not relative: the deadline is unaffected by however long dispatch and
TTFT took before the accumulator was constructed. Checked as well as — not
instead of — the existing ceiling, so every caller without a host deadline is
byte-for-byte unchanged, and the "timed out" phrasing keeps _is_timeout_error
classification identical to a request timeout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKrRS7LVgyHf2WQkEahSwu
2026-09-01 23:56:06 -07:00
Jeffrey Quesnelle c56f8cdd48 Merge pull request #100667 from NousResearch/feat/local-models-squash
feat: local models — managed llama.cpp runtime with one-click desktop  setup
2026-09-01 17:53:28 -04:00
Pedro Fontana b3576a29c3 Merge pull request #97354 from NousResearch/fix/nous-org-model-policy
fix(nous): honour the org model policy in the model pickers
2026-09-01 18:20:18 -03:00
emozilla 43e67d872f feat: local models — managed llama.cpp runtime with one-click desktop setup
Run models locally as a first-class provider. The CLI grows a managed
llama.cpp runtime (engine install, model download, server supervision);
the desktop app grows the full setup and management story on top of it.
GUI surfaces ship behind the desktop --local launch flag (hermes desktop
--local, or the flag on the packaged app); backend routes and the CLI
are always live.

Runtime (hermes_cli/local_runtime/):
- curated GGUF catalog with per-machine variant selection: hardware
  probe (VRAM/RAM/UMA), fit planning with spill accounting, quant choice
  by context window
- derived recommendation: quality-ranked picks gated by a predicted
  decode-speed floor, bandwidth-aware on unified memory; the decision
  table is pinned as a test (pick AND reason per memory class), and the
  Recommended badge explains its pick in a tooltip fed by the resolver's
  actual branch
- engine install + model download with resumable split parts, cumulative
  plan-level progress, and staged-model integrity (a split GGUF counts
  only when every part is present)
- server supervision: spawn/adopt/stop, router mode with per-model load
  progress relayed over SSE, abandoned-request cleanup

Desktop:
- Settings -> Providers -> Local models: one-click quickstart (install
  engine, download the recommended model, boot) plus per-model download/
  activate/eject, fit-ranked catalog with context pills
- model pickers (composer dropdown + Cmd+K) show staged local models,
  in-flight downloads as live progress rows, and load-into-memory bars
- local-setup campaign tip for eligible hardware; System resources
  statusbar widget (GPU/VRAM/RAM); in-chat load progress during sends
- friendly dead-server errors, and failed agent builds retry on the next
  send instead of wedging the session

Co-developed with NVIDIA field feedback on RTX 5090 and DGX Spark.
2026-09-01 16:01:53 -04:00
Mariano Nicolini d7520b2822 fix(aux): seed the shared Nous catalog entry with the pickers' arguments 2026-09-01 16:34:48 -03:00
xxxigm 0bee5ff408 fix(auth): look up keyed custom providers by durable pool slug
hermes auth add stores providers.<key> credentials under the config
slug, but runtime only tried custom:<display-name> and then sent the
no-key-required placeholder. Try the slug first, keep the legacy
namespace as fallback, and thread provider_key/key_env through named
custom resolution.
2026-09-01 22:42:27 +05:30
Teknium 51609a35f6 fix(auth): purge silent OpenRouter paid-default adoption (#81952 class fix)
Three kills at the shared chokepoints:

1. resolve_provider() now REFUSES env-key/pool auto-adoption of openrouter
   while the active config.yaml is corrupt (AuthError code=corrupt_config).
   A broken config falls back to DEFAULT_CONFIG, so tier-2 found no
   model.provider and tier-3/4 silently adopted the PAID openrouter provider
   against the user's real (unparseable) intent. New probe:
   hermes_cli.config.get_active_config_parse_failure(), recorded in the
   existing _warn_config_parse_failure() funnel keyed by (mtime_ns, size) —
   a fixed file clears the block immediately. Explicit provider requests
   are untouched.

2. auxiliary lane built-in OpenRouter fallback model is now a :free SKU
   (nvidia/nemotron-3-ultra-550b-a55b:free) instead of the paid
   google/gemini-3.6-flash. User-configured auxiliary.openrouter_model is
   honored untouched (paid-lane warning retained).

3. env->pool ingestion of OPENROUTER_API_KEY now logs a WARNING (once per
   process per provider) when a credential is newly ingested — ingestion
   itself stays allowed.

Fixes #81952 (silent-paid-default half; sibling PR covers the
non-interactive fail-closed guard).
2026-09-01 07:00:38 -07:00
Brooklyn Nicholson 02458e67ad fix(auxiliary): accept dict and object messages in extract_content_or_reasoning
Compression and some OpenAI-compatible proxies hand us a dict-shaped
response or a bare message, not a ChatCompletion. Reuse the existing
helper instead of a second extractor, and bound an optional reasoning
fallback so a chain-of-thought dump cannot become the summary.

Co-authored-by: Chris DePuy <chris@650group.com>
Co-authored-by: chenhm <chenhm@yuancheng.local>
2026-08-31 20:43:00 -05:00
theo 0a8b25e0ab fix(auxiliary): forward service tier on Codex Responses 2026-08-31 13:00:33 -07:00
liuhao1024 b26a1eae8f fix(auxiliary): preserve max_tokens for OpenRouter to prevent free-tier 402
_build_call_kwargs strips max_tokens for non-Anthropic providers to
avoid wire-format issues (Copilot, ZAI, GPT-5). However, OpenRouter
free/limited-credit tiers need max_tokens because the model's full
output window exceeds the credit budget, causing HTTP 402.

Without max_tokens, the 402 triggers fallback to a text-only model
which then fails with 'unknown variant image_url, expected text'.

Include max_tokens when provider is 'openrouter' or base_url contains
'openrouter.ai'.

Fixes #41035
2026-08-31 13:00:24 -07:00
Teknium 8a766c3f9f fix(auxiliary): stranger-thread timeout also wakes the attempt stream
Socket shutdown() releases readers blocked on a real transport, but the
owner can be blocked inside the SDK event stream (or a transportless
double). Close the attempt-owned stream from the Timer — the same
attempt-scoped wake the hard-cancel branch uses — so the owner unwinds
and performs the real FD release in its finally. Fixes the
test_codex_timeout_and_explicit_cancel_have_one_linearized_outcome red.
2026-08-31 13:00:18 -07:00
dsad 8a5b49d86a fix(auxiliary): never release Codex client FDs from the timeout Timer
_CodexCompletionsAdapter.create arms a daemon threading.Timer that calls
client.close() when the aux Responses stream exceeds its timeout. On a
stalled stream -- the failure the timeout exists for -- the Timer is the
only thing that fires, so the close runs on a thread that does not own
the in-flight httpx connection.

That is the FD-ownership violation the repo already fixed twice on the
main transport (#29507, #67142, #70773): close() releases the raw TLS fd
while the owner's OpenSSL BIO still caches that integer, the kernel
recycles it into the next open() in the process -- a SessionDB or
kanban.db handle -- and the owner's unwinding TLS flush writes an
application-data record into that database file.

agent/auxiliary_client.py had no thread-ownership machinery at all: the
guarded twins (_retire_shared_openai_client, _abort_request_openai_client)
live in run_agent.py and are unreachable from this adapter, which holds no
AIAgent reference.

Dispatch on ownership the way chat_completion_helpers already does: from a
stranger thread only force_close_tcp_sockets() (shutdown(SHUT_RDWR), which
is FD-safe from any thread), and let the owning thread release the FDs when
it unwinds. The owner-thread caller (_check_cancelled) keeps closing
directly. Cache eviction (#23432) is unchanged.
2026-08-31 13:00:18 -07:00
Mariano Nicolini e681decfae fix(aux): policy-check the whole auxiliary model ladder
Only the catalog step was filtered. With no fast-family match in the allowed
catalog it returned empty and the ladder fell through to a public
recommendation, which could hand titling a model the org blocks.
2026-08-31 15:58:27 -03:00
Teknium f50b5bb0fa fix(compression): dead Codex summary streams fail over in 60s instead of stacking 5-minute waits
The Codex auxiliary Responses adapter enforced a single absolute
deadline (300s floor for compression). A dead stream held the entire
budget before fallback ran, and repeated compression attempts stacked
those waits into 20+ minute 'Summarizing thread' stalls (masoria debug
bundle, Aug 31 2026). Meanwhile a healthy-but-slow reasoning summary
was killed at the same absolute deadline even while producing tokens.

Replace the absolute kill with progress-aware deadlines:
- 60s no-progress window for the first substantive payload AND between
  payloads; keepalive/lifecycle frames do not re-arm (mirrors the
  commit-fence gating, #96707)
- a live stream re-arms per token and is bounded only by
  _aux_stream_total_ceiling() (max(600s, 4x configured timeout)), the
  same backstop the streamed chat.completions path already uses
- the compression critical-path retry gate now distinguishes failure
  cost: a cheap first-token no-progress failure retries the same
  provider once; mid-stream stalls and ceiling hits still skip straight
  to provider fallback (#54465 semantics preserved)

Live A/B (real OpenAI SDK against a local SSE server, real adapter):
dead keepalive-only stream: main waits the full budget; fixed fails
over at the window. Slow-but-alive stream (tokens past the configured
timeout): main kills it mid-generation; fixed completes.
2026-08-31 11:52:51 -07:00
anhtahaylove 99d037eeb9 fix(compression): count streamed reasoning details as progress 2026-08-31 11:18:53 -07:00
fangliquanflq fd1d8271db fix(cron): isolate lazy imports from stale modules 2026-08-31 09:58:51 -07:00
joaomarcos 7cbffdd125 refactor(anthropic): split the adapter godfile into four modules
`agent/anthropic_adapter.py` was 3,423 lines and this PR adds another auth
boundary to it. Split along the seams that were already there, so the
credential surface this PR changes has a single owner instead of being
interleaved with request building:

- `agent/anthropic_endpoints.py` (258) — base-URL/endpoint-family predicates.
  Pure functions over a URL string, which is what lets both of the modules
  below depend on it without a cycle.
- `agent/anthropic_message_convert.py` (1,225) — OpenAI-style to Anthropic
  Messages payload conversion: model ids, tool schemas, content/thinking
  blocks, tool_use pairing, cache_control, screenshot eviction, blank-block
  scrubbing.
- `agent/anthropic_credentials.py` (910) — credential sources, the OAuth
  flows, and the refresh commit (`CredentialPersistError` and both singleton
  writers).
- `agent/anthropic_adapter.py` (1,215) — client construction and the Messages
  API call, re-exporting every name from the three modules above so existing
  `from agent.anthropic_adapter import ...` imports keep resolving. The
  re-export surface was diffed against the pre-split module: nothing dropped.

Call sites that read a moved name through the adapter's namespace at runtime
(`credential_pool._refresh_entry_impl`, `auxiliary_client`) now import it from
the defining module, so there is one patchable seam rather than two bindings
that can disagree. The tests that monkeypatched those seams were retargeted to
match; no assertion was changed.

No behavior change.
2026-08-29 18:34:35 -07:00
simonweng 0fb5cab0d4 feat:add hy4-preview model and tokenplan provider 2026-08-29 20:51:17 +05:30
kshitijk4poor 7eee066c30 refactor: fold simplify-code review findings into #96667 salvage
- Anthropic classifier counts signature_delta and citations_delta payloads
  (content-bearing delta types the transport emits — relay_llm.py handles
  both) so signed-thinking/cited-text generation keeps ticking the fence.
- Fix the stale 'per streamed event' comment above the Anthropic
  on_stream_event lambda (left over from the conflict resolution).
- Rename test_completed_response_without_stream_payload_does_not_tick to
  test_completed_response_ticks_only_terminal_signals — the old name
  contradicted its own assertion (dispatch + shim ticks are expected).
2026-08-29 11:08:11 +05:30
StanleyStetson 7ff70f1709 fix(agent): count only substantive auxiliary progress
Salvages the stream-progress fix from #80122 on current main while incorporating review feedback for empty provider deltas and tool-call scaffolding.
2026-08-29 11:08:11 +05:30